From 244d6a836795bf07dacd6b766436dbd6bf5fa912 Mon Sep 17 00:00:00 2001
From: surbhigarg92 <surbhigarg.92@gmail.com>
Date: Thu, 8 Feb 2024 08:08:16 +0000
Subject: [PATCH] feat: Open telemetry implementation (#2770)

This PR adds support for [OpenTelemetry](https://opentelemetry.io/) Instrumentation for Traces and Metrics.

Add dependency for [OpenTelemetrySDK](https://opentelemetry.io/docs/instrumentation/java/manual/#initialize-the-sdk) and required [exporters](https://opentelemetry.io/docs/instrumentation/java/exporters/).

Create OpenTelemetry object with required MeterProvider and TracerProvider exporter . Inject OpenTelemetry object via SpannerOptions or register as Global

`
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
              .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
              .setTracerProvider(tracerProvider)
              .setMeterProvider(sdkMeterProvider)
              .build;

SpannerOptions options = SpannerOptions.newBuilder().setOpenTelemetry(openTelemetry).build();
`

 By default, OpenTelemetry traces are not enabled. To enable OpenTelemetry traces , call `SpannerOptions.enableOpenTelemetryTraces()` in startup of your application. Enabling OpenTelemetry traces will disable OpenCensus traces. Both OpenCensus and OpenTelemetry traces can not be enabled at the same time.
---
 .gitignore                                    |   1 +
 .../clirr-ignored-differences.xml             |   7 +-
 google-cloud-spanner/pom.xml                  |  32 +-
 .../cloud/spanner/AbstractReadContext.java    |  30 +-
 .../cloud/spanner/AbstractResultSet.java      |  46 +-
 .../spanner/AsyncTransactionManagerImpl.java  |  10 +-
 .../google/cloud/spanner/BatchClientImpl.java |  11 +-
 .../cloud/spanner/DatabaseClientImpl.java     | 117 ++--
 .../java/com/google/cloud/spanner/IScope.java |  26 +
 .../java/com/google/cloud/spanner/ISpan.java  |  45 ++
 .../spanner/MetricRegistryConstants.java      |  26 +-
 .../google/cloud/spanner/OpenCensusScope.java |  33 +
 .../google/cloud/spanner/OpenCensusSpan.java  | 111 ++++
 .../cloud/spanner/OpenTelemetryScope.java     |  33 +
 .../cloud/spanner/OpenTelemetrySpan.java      |  99 +++
 .../spanner/PartitionedDmlTransaction.java    |   7 +-
 .../google/cloud/spanner/SessionClient.java   |  38 +-
 .../com/google/cloud/spanner/SessionImpl.java | 101 ++--
 .../com/google/cloud/spanner/SessionPool.java | 210 +++++--
 .../com/google/cloud/spanner/SpannerImpl.java |  30 +-
 .../google/cloud/spanner/SpannerOptions.java  | 116 ++++
 .../cloud/spanner/SpannerRpcMetrics.java      |  74 +++
 .../com/google/cloud/spanner/TraceUtil.java   |  82 ---
 .../google/cloud/spanner/TraceWrapper.java    | 109 ++++
 .../cloud/spanner/TransactionManagerImpl.java |  22 +-
 .../cloud/spanner/TransactionRunnerImpl.java  | 119 ++--
 .../cloud/spanner/spi/v1/GapicSpannerRpc.java |   2 +-
 .../spanner/spi/v1/HeaderInterceptor.java     |  68 ++-
 .../spi/v1/SpannerInterceptorProvider.java    |  24 +-
 .../cloud/spanner/spi/v1/SpannerRpcViews.java |  31 +-
 .../AsyncTransactionManagerImplTest.java      |   8 +-
 .../cloud/spanner/BatchClientImplTest.java    |   9 +
 .../cloud/spanner/DatabaseClientImplTest.java |  13 +-
 .../FailOnOverkillTraceComponentImpl.java     |  29 +-
 .../spanner/ITSessionPoolIntegrationTest.java |   6 +-
 .../IntegrationTestWithClosedSessionsEnv.java |   6 +-
 .../cloud/spanner/OpenTelemetrySpanTest.java  | 563 ++++++++++++++++++
 .../spanner/ResumableStreamIteratorTest.java  |  44 +-
 .../cloud/spanner/SessionClientTests.java     |  11 +
 .../google/cloud/spanner/SessionImplTest.java |  18 +-
 .../spanner/SessionPoolMaintainerTest.java    |  10 +-
 .../cloud/spanner/SessionPoolStressTest.java  |   6 +-
 .../google/cloud/spanner/SessionPoolTest.java | 303 +++++++++-
 .../com/google/cloud/spanner/SpanTest.java    | 108 ++++
 .../google/cloud/spanner/SpannerImplTest.java |   9 +
 .../cloud/spanner/SpannerOptionsTest.java     |  27 +
 .../spanner/TransactionContextImplTest.java   |  12 +
 .../spanner/TransactionManagerImplTest.java   |  18 +-
 .../spanner/TransactionRunnerImplTest.java    |  32 +-
 .../cloud/spanner/spi/v1/GfeLatencyTest.java  |   3 -
 .../spanner/spi/v1/SpannerRpcMetricsTest.java | 299 ++++++++++
 pom.xml                                       |   8 +
 .../com/example/spanner/TracingSample.java    |   6 +-
 53 files changed, 2719 insertions(+), 489 deletions(-)
 create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/IScope.java
 create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/ISpan.java
 create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenCensusScope.java
 create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenCensusSpan.java
 create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenTelemetryScope.java
 create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenTelemetrySpan.java
 create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerRpcMetrics.java
 delete mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceUtil.java
 create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java
 create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetrySpanTest.java
 create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/SpannerRpcMetricsTest.java

diff --git a/.gitignore b/.gitignore
index ccf024c145b..0141aa5d4c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ nosetests.xml
 .settings
 .DS_Store
 .classpath
+.tool-versions
 
 # Built documentation
 docs/
diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml
index 76d2aa7c79f..54eae11d51b 100644
--- a/google-cloud-spanner/clirr-ignored-differences.xml
+++ b/google-cloud-spanner/clirr-ignored-differences.xml
@@ -426,7 +426,6 @@
     <className>com/google/cloud/spanner/connection/Connection</className>
     <method>void rollbackToSavepoint(java.lang.String)</method>
   </difference>
-  
   <!-- Delay start transaction -->
   <difference>
     <differenceType>7012</differenceType>
@@ -540,6 +539,12 @@
     <className>com/google/cloud/spanner/Dialect</className>
     <method>java.lang.String getDefaultSchema()</method>
   </difference>
+  <difference>
+    <differenceType>7005</differenceType>
+    <className>com/google/cloud/spanner/PartitionedDmlTransaction</className>
+    <method>void setSpan(io.opencensus.trace.Span)</method>
+    <to>void setSpan(com.google.cloud.spanner.ISpan)</to>
+  </difference>
   
   <!-- Added DirectedReadOptions -->
   <difference>
diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml
index b3aa2b88943..90f1d73c6e6 100644
--- a/google-cloud-spanner/pom.xml
+++ b/google-cloud-spanner/pom.xml
@@ -247,6 +247,14 @@
       <artifactId>opencensus-impl</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>io.opentelemetry</groupId>
+      <artifactId>opentelemetry-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.opentelemetry</groupId>
+      <artifactId>opentelemetry-context</artifactId>
+    </dependency>
     <dependency>
       <groupId>com.google.auth</groupId>
       <artifactId>google-auth-library-oauth2-http</artifactId>
@@ -393,7 +401,6 @@
       <version>2.2</version>
       <scope>test</scope>
     </dependency>
-    
     <!-- Benchmarking dependencies -->
     <dependency>
       <groupId>org.openjdk.jmh</groupId>
@@ -407,9 +414,28 @@
       <version>1.37</version>
       <scope>test</scope>
     </dependency>
-
+    <!-- OpenTelemetry test dependencies -->
+    <dependency>
+      <groupId>io.opentelemetry</groupId>
+      <artifactId>opentelemetry-sdk</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.opentelemetry</groupId>
+      <artifactId>opentelemetry-sdk-metrics</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.opentelemetry</groupId>
+      <artifactId>opentelemetry-sdk-trace</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.opentelemetry</groupId>
+      <artifactId>opentelemetry-sdk-testing</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
-
   <profiles>
     <profile>
       <id>java9</id>
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java
index 0f4310f9b4d..0714b7651cc 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java
@@ -39,6 +39,7 @@
 import com.google.cloud.spanner.spi.v1.SpannerRpc;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.protobuf.ByteString;
 import com.google.spanner.v1.BeginTransactionRequest;
@@ -53,8 +54,6 @@
 import com.google.spanner.v1.Transaction;
 import com.google.spanner.v1.TransactionOptions;
 import com.google.spanner.v1.TransactionSelector;
-import io.opencensus.trace.Span;
-import io.opencensus.trace.Tracing;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicLong;
 import javax.annotation.Nullable;
@@ -70,7 +69,8 @@ abstract class AbstractReadContext
   abstract static class Builder<B extends Builder<?, T>, T extends AbstractReadContext> {
     private SessionImpl session;
     private SpannerRpc rpc;
-    private Span span = Tracing.getTracer().getCurrentSpan();
+    private ISpan span;
+    private TraceWrapper tracer;
     private int defaultPrefetchChunks = SpannerOptions.Builder.DEFAULT_PREFETCH_CHUNKS;
     private QueryOptions defaultQueryOptions = SpannerOptions.Builder.DEFAULT_QUERY_OPTIONS;
     private DirectedReadOptions defaultDirectedReadOption;
@@ -94,11 +94,16 @@ B setRpc(SpannerRpc rpc) {
       return self();
     }
 
-    B setSpan(Span span) {
+    B setSpan(ISpan span) {
       this.span = span;
       return self();
     }
 
+    B setTracer(TraceWrapper tracer) {
+      this.tracer = tracer;
+      return self();
+    }
+
     B setDefaultPrefetchChunks(int defaultPrefetchChunks) {
       this.defaultPrefetchChunks = defaultPrefetchChunks;
       return self();
@@ -389,9 +394,12 @@ void initTransaction() {
           }
           transactionId = transaction.getId();
           span.addAnnotation(
-              "Transaction Creation Done", TraceUtil.getTransactionAnnotations(transaction));
+              "Transaction Creation Done",
+              ImmutableMap.of(
+                  "Id", transaction.getId().toStringUtf8(), "Timestamp", timestamp.toString()));
+
         } catch (SpannerException e) {
-          span.addAnnotation("Transaction Creation Failed", TraceUtil.getExceptionAnnotations(e));
+          span.addAnnotation("Transaction Creation Failed", e);
           throw e;
         }
       }
@@ -402,7 +410,8 @@ void initTransaction() {
   final SessionImpl session;
   final SpannerRpc rpc;
   final ExecutorProvider executorProvider;
-  Span span;
+  ISpan span;
+  TraceWrapper tracer;
   private final int defaultPrefetchChunks;
   private final QueryOptions defaultQueryOptions;
 
@@ -435,10 +444,11 @@ void initTransaction() {
     this.span = builder.span;
     this.executorProvider = builder.executorProvider;
     this.clock = builder.clock;
+    this.tracer = builder.tracer;
   }
 
   @Override
-  public void setSpan(Span span) {
+  public void setSpan(ISpan span) {
     this.span = span;
   }
 
@@ -692,6 +702,7 @@ ResultSet executeQueryInternalWithOptions(
             MAX_BUFFERED_CHUNKS,
             SpannerImpl.QUERY,
             span,
+            tracer,
             rpc.getExecuteQueryRetrySettings(),
             rpc.getExecuteQueryRetryableCodes()) {
           @Override
@@ -752,7 +763,7 @@ public final void invalidate() {
 
   @Override
   public void close() {
-    span.end(TraceUtil.END_SPAN_OPTIONS);
+    span.end();
     synchronized (lock) {
       isClosed = true;
     }
@@ -837,6 +848,7 @@ ResultSet readInternalWithOptions(
             MAX_BUFFERED_CHUNKS,
             SpannerImpl.READ,
             span,
+            tracer,
             rpc.getReadRetrySettings(),
             rpc.getReadRetryableCodes()) {
           @Override
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java
index c18e64165bc..d946257bc45 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java
@@ -37,7 +37,6 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.AbstractIterator;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.io.CharSource;
 import com.google.common.util.concurrent.Uninterruptibles;
@@ -53,11 +52,6 @@
 import com.google.spanner.v1.Transaction;
 import com.google.spanner.v1.TypeCode;
 import io.grpc.Context;
-import io.opencensus.common.Scope;
-import io.opencensus.trace.AttributeValue;
-import io.opencensus.trace.Span;
-import io.opencensus.trace.Tracer;
-import io.opencensus.trace.Tracing;
 import java.io.IOException;
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -87,7 +81,6 @@
 
 /** Implementation of {@link ResultSet}. */
 abstract class AbstractResultSet<R> extends AbstractStructReader implements ResultSet {
-  private static final Tracer tracer = Tracing.getTracer();
   private static final com.google.protobuf.Value NULL_VALUE =
       com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build();
 
@@ -1206,7 +1199,8 @@ abstract static class ResumableStreamIterator extends AbstractIterator<PartialRe
     private final BackOff backOff;
     private final LinkedList<PartialResultSet> buffer = new LinkedList<>();
     private final int maxBufferSize;
-    private final Span span;
+    private final ISpan span;
+    private final TraceWrapper tracer;
     private CloseableIterator<PartialResultSet> stream;
     private ByteString resumeToken;
     private boolean finished;
@@ -1220,12 +1214,14 @@ abstract static class ResumableStreamIterator extends AbstractIterator<PartialRe
     protected ResumableStreamIterator(
         int maxBufferSize,
         String streamName,
-        Span parent,
+        ISpan parent,
+        TraceWrapper tracer,
         RetrySettings streamingRetrySettings,
         Set<Code> retryableCodes) {
       checkArgument(maxBufferSize >= 0);
       this.maxBufferSize = maxBufferSize;
-      this.span = tracer.spanBuilderWithExplicitParent(streamName, parent).startSpan();
+      this.tracer = tracer;
+      this.span = tracer.spanBuilderWithExplicitParent(streamName, parent);
       this.streamingRetrySettings = Preconditions.checkNotNull(streamingRetrySettings);
       this.retryableCodes = Preconditions.checkNotNull(retryableCodes);
       this.backOff = newBackOff();
@@ -1281,11 +1277,7 @@ private static long nextBackOffMillis(BackOff backoff) throws SpannerException {
     }
 
     private void backoffSleep(Context context, long backoffMillis) throws SpannerException {
-      tracer
-          .getCurrentSpan()
-          .addAnnotation(
-              "Backing off",
-              ImmutableMap.of("Delay", AttributeValue.longAttributeValue(backoffMillis)));
+      tracer.getCurrentSpan().addAnnotation("Backing off", "Delay", backoffMillis);
       final CountDownLatch latch = new CountDownLatch(1);
       final Context.CancellationListener listener =
           ignored -> {
@@ -1325,7 +1317,7 @@ public void execute(Runnable command) {
     public void close(@Nullable String message) {
       if (stream != null) {
         stream.close(message);
-        span.end(TraceUtil.END_SPAN_OPTIONS);
+        span.end();
         stream = null;
       }
     }
@@ -1343,11 +1335,9 @@ protected PartialResultSet computeNext() {
         if (stream == null) {
           span.addAnnotation(
               "Starting/Resuming stream",
-              ImmutableMap.of(
-                  "ResumeToken",
-                  AttributeValue.stringAttributeValue(
-                      resumeToken == null ? "null" : resumeToken.toStringUtf8())));
-          try (Scope s = tracer.withSpan(span)) {
+              "ResumeToken",
+              resumeToken == null ? "null" : resumeToken.toStringUtf8());
+          try (IScope scope = tracer.withSpan(span)) {
             // When start a new stream set the Span as current to make the gRPC Span a child of
             // this Span.
             stream = checkNotNull(startStream(resumeToken));
@@ -1387,9 +1377,7 @@ protected PartialResultSet computeNext() {
           }
         } catch (SpannerException spannerException) {
           if (safeToRetry && isRetryable(spannerException)) {
-            span.addAnnotation(
-                "Stream broken. Safe to retry",
-                TraceUtil.getExceptionAnnotations(spannerException));
+            span.addAnnotation("Stream broken. Safe to retry", spannerException);
             logger.log(Level.FINE, "Retryable exception, will sleep and retry", spannerException);
             // Truncate any items in the buffer before the last retry token.
             while (!buffer.isEmpty() && buffer.getLast().getResumeToken().isEmpty()) {
@@ -1397,7 +1385,7 @@ protected PartialResultSet computeNext() {
             }
             assert buffer.isEmpty() || buffer.getLast().getResumeToken().equals(resumeToken);
             stream = null;
-            try (Scope s = tracer.withSpan(span)) {
+            try (IScope s = tracer.withSpan(span)) {
               long delay = spannerException.getRetryDelayInMillis();
               if (delay != -1) {
                 backoffSleep(context, delay);
@@ -1408,12 +1396,12 @@ protected PartialResultSet computeNext() {
 
             continue;
           }
-          span.addAnnotation("Stream broken. Not safe to retry");
-          TraceUtil.setWithFailure(span, spannerException);
+          span.addAnnotation("Stream broken. Not safe to retry", spannerException);
+          span.setStatus(spannerException);
           throw spannerException;
         } catch (RuntimeException e) {
-          span.addAnnotation("Stream broken. Not safe to retry");
-          TraceUtil.setWithFailure(span, e);
+          span.addAnnotation("Stream broken. Not safe to retry", e);
+          span.setStatus(e);
           throw e;
         }
       }
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java
index 510bd02bc0e..8b20dd824a0 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java
@@ -28,31 +28,27 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
 import com.google.common.util.concurrent.MoreExecutors;
-import io.opencensus.trace.Span;
-import io.opencensus.trace.Tracer;
-import io.opencensus.trace.Tracing;
 
 /** Implementation of {@link AsyncTransactionManager}. */
 final class AsyncTransactionManagerImpl
     implements CommittableAsyncTransactionManager, SessionTransaction {
-  private static final Tracer tracer = Tracing.getTracer();
 
   private final SessionImpl session;
-  private Span span;
+  private ISpan span;
   private final Options options;
 
   private TransactionRunnerImpl.TransactionContextImpl txn;
   private TransactionState txnState;
   private final SettableApiFuture<CommitResponse> commitResponse = SettableApiFuture.create();
 
-  AsyncTransactionManagerImpl(SessionImpl session, Span span, TransactionOption... options) {
+  AsyncTransactionManagerImpl(SessionImpl session, ISpan span, TransactionOption... options) {
     this.session = session;
     this.span = span;
     this.options = Options.fromTransactionOptions(options);
   }
 
   @Override
-  public void setSpan(Span span) {
+  public void setSpan(ISpan span) {
     this.span = span;
   }
 
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java
index eab90a266c9..664cde1edbb 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java
@@ -30,7 +30,6 @@
 import com.google.spanner.v1.PartitionReadRequest;
 import com.google.spanner.v1.PartitionResponse;
 import com.google.spanner.v1.TransactionSelector;
-import io.opencensus.trace.Tracing;
 import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
@@ -62,7 +61,9 @@ public BatchReadOnlyTransaction batchReadOnlyTransaction(TimestampBound bound) {
             .setExecutorProvider(sessionClient.getSpanner().getAsyncExecutorProvider())
             .setDefaultPrefetchChunks(sessionClient.getSpanner().getDefaultPrefetchChunks())
             .setDefaultDirectedReadOptions(
-                sessionClient.getSpanner().getOptions().getDirectedReadOptions()),
+                sessionClient.getSpanner().getOptions().getDirectedReadOptions())
+            .setSpan(sessionClient.getSpanner().getTracer().getCurrentSpan())
+            .setTracer(sessionClient.getSpanner().getTracer()),
         checkNotNull(bound));
   }
 
@@ -81,7 +82,9 @@ public BatchReadOnlyTransaction batchReadOnlyTransaction(BatchTransactionId batc
             .setExecutorProvider(sessionClient.getSpanner().getAsyncExecutorProvider())
             .setDefaultPrefetchChunks(sessionClient.getSpanner().getDefaultPrefetchChunks())
             .setDefaultDirectedReadOptions(
-                sessionClient.getSpanner().getOptions().getDirectedReadOptions()),
+                sessionClient.getSpanner().getOptions().getDirectedReadOptions())
+            .setSpan(sessionClient.getSpanner().getTracer().getCurrentSpan())
+            .setTracer(sessionClient.getSpanner().getTracer()),
         batchTransactionId);
   }
 
@@ -95,7 +98,6 @@ private static class BatchReadOnlyTransactionImpl extends MultiUseReadOnlyTransa
       super(builder.setTimestampBound(bound));
       this.sessionName = session.getName();
       this.options = session.getOptions();
-      setSpan(Tracing.getTracer().getCurrentSpan());
       initTransaction();
     }
 
@@ -104,7 +106,6 @@ private static class BatchReadOnlyTransactionImpl extends MultiUseReadOnlyTransa
       super(builder.setTransactionId(batchTransactionId.getTransactionId()));
       this.sessionName = session.getName();
       this.options = session.getOptions();
-      setSpan(Tracing.getTracer().getCurrentSpan());
     }
 
     @Override
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java
index 3835cb1f338..b63ad379305 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java
@@ -26,29 +26,25 @@
 import com.google.common.base.Function;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.spanner.v1.BatchWriteResponse;
-import io.opencensus.common.Scope;
-import io.opencensus.trace.Span;
-import io.opencensus.trace.Tracer;
-import io.opencensus.trace.Tracing;
 import javax.annotation.Nullable;
 
 class DatabaseClientImpl implements DatabaseClient {
   private static final String READ_WRITE_TRANSACTION = "CloudSpanner.ReadWriteTransaction";
   private static final String READ_ONLY_TRANSACTION = "CloudSpanner.ReadOnlyTransaction";
   private static final String PARTITION_DML_TRANSACTION = "CloudSpanner.PartitionDMLTransaction";
-  private static final Tracer tracer = Tracing.getTracer();
-
+  private final TraceWrapper tracer;
   @VisibleForTesting final String clientId;
   @VisibleForTesting final SessionPool pool;
 
   @VisibleForTesting
-  DatabaseClientImpl(SessionPool pool) {
-    this("", pool);
+  DatabaseClientImpl(SessionPool pool, TraceWrapper tracer) {
+    this("", pool, tracer);
   }
 
-  DatabaseClientImpl(String clientId, SessionPool pool) {
+  DatabaseClientImpl(String clientId, SessionPool pool, TraceWrapper tracer) {
     this.clientId = clientId;
     this.pool = pool;
+    this.tracer = tracer;
   }
 
   @VisibleForTesting
@@ -76,14 +72,14 @@ public Timestamp write(final Iterable<Mutation> mutations) throws SpannerExcepti
   public CommitResponse writeWithOptions(
       final Iterable<Mutation> mutations, final TransactionOption... options)
       throws SpannerException {
-    Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return runWithSessionRetry(session -> session.writeWithOptions(mutations, options));
     } catch (RuntimeException e) {
-      TraceUtil.setWithFailure(span, e);
+      span.setStatus(e);
       throw e;
     } finally {
-      span.end(TraceUtil.END_SPAN_OPTIONS);
+      span.end();
     }
   }
 
@@ -96,15 +92,15 @@ public Timestamp writeAtLeastOnce(final Iterable<Mutation> mutations) throws Spa
   public CommitResponse writeAtLeastOnceWithOptions(
       final Iterable<Mutation> mutations, final TransactionOption... options)
       throws SpannerException {
-    Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return runWithSessionRetry(
           session -> session.writeAtLeastOnceWithOptions(mutations, options));
     } catch (RuntimeException e) {
-      TraceUtil.setWithFailure(span, e);
+      span.setStatus(e);
       throw e;
     } finally {
-      span.end(TraceUtil.END_SPAN_OPTIONS);
+      span.end();
     }
   }
 
@@ -112,136 +108,145 @@ public CommitResponse writeAtLeastOnceWithOptions(
   public ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
       final Iterable<MutationGroup> mutationGroups, final TransactionOption... options)
       throws SpannerException {
-    Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return runWithSessionRetry(session -> session.batchWriteAtLeastOnce(mutationGroups, options));
     } catch (RuntimeException e) {
-      TraceUtil.setWithFailure(span, e);
+      span.setStatus(e);
       throw e;
     } finally {
-      span.end(TraceUtil.END_SPAN_OPTIONS);
+      span.end();
     }
   }
 
   @Override
   public ReadContext singleUse() {
-    Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return getSession().singleUse();
     } catch (RuntimeException e) {
-      TraceUtil.endSpanWithFailure(span, e);
+      span.setStatus(e);
+      span.end();
       throw e;
     }
   }
 
   @Override
   public ReadContext singleUse(TimestampBound bound) {
-    Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return getSession().singleUse(bound);
     } catch (RuntimeException e) {
-      TraceUtil.endSpanWithFailure(span, e);
+      span.setStatus(e);
+      span.end();
       throw e;
     }
   }
 
   @Override
   public ReadOnlyTransaction singleUseReadOnlyTransaction() {
-    Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return getSession().singleUseReadOnlyTransaction();
     } catch (RuntimeException e) {
-      TraceUtil.endSpanWithFailure(span, e);
+      span.setStatus(e);
+      span.end();
       throw e;
     }
   }
 
   @Override
   public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) {
-    Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return getSession().singleUseReadOnlyTransaction(bound);
     } catch (RuntimeException e) {
-      TraceUtil.endSpanWithFailure(span, e);
+      span.setStatus(e);
+      span.end();
       throw e;
     }
   }
 
   @Override
   public ReadOnlyTransaction readOnlyTransaction() {
-    Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return getSession().readOnlyTransaction();
     } catch (RuntimeException e) {
-      TraceUtil.endSpanWithFailure(span, e);
+      span.setStatus(e);
+      span.end();
       throw e;
     }
   }
 
   @Override
   public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) {
-    Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return getSession().readOnlyTransaction(bound);
     } catch (RuntimeException e) {
-      TraceUtil.endSpanWithFailure(span, e);
+      span.setStatus(e);
+      span.end();
       throw e;
     }
   }
 
   @Override
   public TransactionRunner readWriteTransaction(TransactionOption... options) {
-    Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return getSession().readWriteTransaction(options);
     } catch (RuntimeException e) {
-      TraceUtil.endSpanWithFailure(span, e);
+      span.setStatus(e);
+      span.end();
       throw e;
-    } finally {
-      span.end(TraceUtil.END_SPAN_OPTIONS);
     }
   }
 
   @Override
   public TransactionManager transactionManager(TransactionOption... options) {
-    Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return getSession().transactionManager(options);
     } catch (RuntimeException e) {
-      TraceUtil.endSpanWithFailure(span, e);
+      span.setStatus(e);
+      span.end();
       throw e;
     }
   }
 
   @Override
   public AsyncRunner runAsync(TransactionOption... options) {
-    Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return getSession().runAsync(options);
     } catch (RuntimeException e) {
-      TraceUtil.endSpanWithFailure(span, e);
+      span.setStatus(e);
+      span.end();
       throw e;
     }
   }
 
   @Override
   public AsyncTransactionManager transactionManagerAsync(TransactionOption... options) {
-    Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return getSession().transactionManagerAsync(options);
     } catch (RuntimeException e) {
-      TraceUtil.endSpanWithFailure(span, e);
+      span.setStatus(e);
+      span.end();
       throw e;
     }
   }
 
   @Override
   public long executePartitionedUpdate(final Statement stmt, final UpdateOption... options) {
-    Span span = tracer.spanBuilder(PARTITION_DML_TRANSACTION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(PARTITION_DML_TRANSACTION);
+    try (IScope s = tracer.withSpan(span)) {
       return runWithSessionRetry(session -> session.executePartitionedUpdate(stmt, options));
     } catch (RuntimeException e) {
-      TraceUtil.endSpanWithFailure(span, e);
+      span.setStatus(e);
+      span.end();
       throw e;
     }
   }
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/IScope.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/IScope.java
new file mode 100644
index 00000000000..cbefe47b887
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/IScope.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 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;
+
+/**
+ * This interface represents a scope that wraps both OpenCensus and OpenTelemetry scopes. It extends
+ * the AutoCloseable interface and overrides the close method that does not throw an exception.
+ */
+interface IScope extends AutoCloseable {
+  @Override
+  void close();
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ISpan.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ISpan.java
new file mode 100644
index 00000000000..ce837de0e58
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ISpan.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 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 java.util.Map;
+
+interface ISpan {
+
+  /**
+   * Adds an annotation to the OpenCensus and OpenTelemetry span.
+   *
+   * @param message the description of the annotation event.
+   * @param attributes the map of attribute key-value pairs that will be added; these are associated
+   *     with this annotation.
+   */
+  void addAnnotation(String message, Map<String, Object> attributes);
+
+  void addAnnotation(String message);
+
+  void addAnnotation(String message, String key, String value);
+
+  void addAnnotation(String message, String key, long value);
+
+  void addAnnotation(String message, Throwable e);
+
+  void setStatus(Throwable e);
+
+  void setStatus(ErrorCode errorCode);
+
+  void end();
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MetricRegistryConstants.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MetricRegistryConstants.java
index 3512a75732d..d0098e961e6 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MetricRegistryConstants.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MetricRegistryConstants.java
@@ -65,13 +65,20 @@ class MetricRegistryConstants {
   /** Unit to represent counts. */
   static final String COUNT = "1";
 
+  static final String INSTRUMENTATION_SCOPE = "cloud.google.com/java";
+
+  static final String METRIC_PREFIX = "cloud.google.com/java/";
+
   // The Metric name and description
-  static final String MAX_IN_USE_SESSIONS = "cloud.google.com/java/spanner/max_in_use_sessions";
-  static final String MAX_ALLOWED_SESSIONS = "cloud.google.com/java/spanner/max_allowed_sessions";
-  static final String GET_SESSION_TIMEOUTS = "cloud.google.com/java/spanner/get_session_timeouts";
-  static final String NUM_ACQUIRED_SESSIONS = "cloud.google.com/java/spanner/num_acquired_sessions";
-  static final String NUM_RELEASED_SESSIONS = "cloud.google.com/java/spanner/num_released_sessions";
-  static final String NUM_SESSIONS_IN_POOL = "cloud.google.com/java/spanner/num_sessions_in_pool";
+  static final String MAX_IN_USE_SESSIONS = "spanner/max_in_use_sessions";
+  static final String MAX_ALLOWED_SESSIONS = "spanner/max_allowed_sessions";
+  static final String GET_SESSION_TIMEOUTS = "spanner/get_session_timeouts";
+  static final String NUM_ACQUIRED_SESSIONS = "spanner/num_acquired_sessions";
+  static final String NUM_RELEASED_SESSIONS = "spanner/num_released_sessions";
+  static final String NUM_SESSIONS_IN_POOL = "spanner/num_sessions_in_pool";
+  static final String NUM_SESSIONS_IN_USE = "spanner/num_in_use_sessions";
+  static final String NUM_SESSIONS_AVAILABLE = "spanner/num_available_sessions";
+  static final String SESSIONS_TYPE = "session_type";
 
   static final String MAX_IN_USE_SESSIONS_DESCRIPTION =
       "The maximum number of sessions in use during the last 10 minute interval.";
@@ -84,4 +91,11 @@ class MetricRegistryConstants {
   static final String NUM_RELEASED_SESSIONS_DESCRIPTION =
       "The number of sessions released by the user and pool maintainer.";
   static final String NUM_SESSIONS_IN_POOL_DESCRIPTION = "The number of sessions in the pool.";
+
+  static final String SPANNER_GFE_LATENCY = "spanner/gfe_latency";
+  static final String SPANNER_GFE_LATENCY_DESCRIPTION =
+      "Latency between Google's network receiving an RPC and reading back the first byte of the response";
+  static final String SPANNER_GFE_HEADER_MISSING_COUNT = "spanner/gfe_header_missing_count";
+  static final String SPANNER_GFE_HEADER_MISSING_COUNT_DESCRIPTION =
+      "Number of RPC responses received without the server-timing header, most likely means that the RPC never reached Google's network";
 }
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenCensusScope.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenCensusScope.java
new file mode 100644
index 00000000000..81c1db16571
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenCensusScope.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 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 io.opencensus.common.Scope;
+
+class OpenCensusScope implements IScope {
+
+  private final Scope openCensusScope;
+
+  OpenCensusScope(Scope openCensusScope) {
+    this.openCensusScope = openCensusScope;
+  }
+
+  @Override
+  public void close() {
+    openCensusScope.close();
+  }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenCensusSpan.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenCensusSpan.java
new file mode 100644
index 00000000000..86e43778f31
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenCensusSpan.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2023 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.common.collect.ImmutableMap;
+import io.opencensus.contrib.grpc.util.StatusConverter;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Status;
+import java.util.HashMap;
+import java.util.Map;
+
+public class OpenCensusSpan implements ISpan {
+
+  static final EndSpanOptions END_SPAN_OPTIONS =
+      EndSpanOptions.builder().setSampleToLocalSpanStore(true).build();
+  private final Span openCensusSpan;
+
+  public OpenCensusSpan(Span openCensusSpan) {
+    this.openCensusSpan = openCensusSpan;
+  }
+
+  Span getOpenCensusSpan() {
+    return openCensusSpan;
+  }
+
+  private ImmutableMap<String, AttributeValue> getOpenCensusExceptionAnnotations(Throwable e) {
+    if (e instanceof SpannerException) {
+      return ImmutableMap.of(
+          "Status",
+          AttributeValue.stringAttributeValue(((SpannerException) e).getErrorCode().toString()));
+    }
+    return ImmutableMap.of();
+  }
+
+  @Override
+  public void addAnnotation(String message, Map<String, Object> attributes) {
+    Map<String, AttributeValue> ocAttributeValues = new HashMap<>();
+    for (Map.Entry<String, Object> entry : attributes.entrySet()) {
+      String key = entry.getKey();
+      Object value = entry.getValue();
+      if (value instanceof String) {
+        ocAttributeValues.put(key, AttributeValue.stringAttributeValue((String) value));
+      } else if (value instanceof Long) {
+        ocAttributeValues.put(key, AttributeValue.longAttributeValue((Long) value));
+      }
+    }
+
+    if (ocAttributeValues.size() > 0) {
+      openCensusSpan.addAnnotation(message, ocAttributeValues);
+    }
+  }
+
+  @Override
+  public void addAnnotation(String message) {
+    openCensusSpan.addAnnotation(message);
+  }
+
+  @Override
+  public void addAnnotation(String message, String key, String value) {
+    openCensusSpan.addAnnotation(
+        message, ImmutableMap.of(key, AttributeValue.stringAttributeValue(value)));
+  }
+
+  @Override
+  public void addAnnotation(String message, String key, long value) {
+    openCensusSpan.addAnnotation(
+        message, ImmutableMap.of(key, AttributeValue.longAttributeValue(value)));
+  }
+
+  @Override
+  public void addAnnotation(String message, Throwable e) {
+    openCensusSpan.addAnnotation(message, this.getOpenCensusExceptionAnnotations(e));
+  }
+
+  @Override
+  public void setStatus(Throwable e) {
+    if (e instanceof SpannerException) {
+      openCensusSpan.setStatus(
+          StatusConverter.fromGrpcStatus(((SpannerException) e).getErrorCode().getGrpcStatus())
+              .withDescription(e.getMessage()));
+    } else {
+      openCensusSpan.setStatus(Status.INTERNAL.withDescription(e.getMessage()));
+    }
+  }
+
+  @Override
+  public void setStatus(ErrorCode errorCode) {
+    openCensusSpan.setStatus(StatusConverter.fromGrpcStatus(errorCode.getGrpcStatus()));
+  }
+
+  @Override
+  public void end() {
+    openCensusSpan.end(END_SPAN_OPTIONS);
+  }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenTelemetryScope.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenTelemetryScope.java
new file mode 100644
index 00000000000..6766bc54e6b
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenTelemetryScope.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 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 io.opentelemetry.context.Scope;
+
+class OpenTelemetryScope implements IScope {
+
+  private final Scope openTelemetryScope;
+
+  OpenTelemetryScope(Scope openTelemetryScope) {
+    this.openTelemetryScope = openTelemetryScope;
+  }
+
+  @Override
+  public void close() {
+    openTelemetryScope.close();
+  }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenTelemetrySpan.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenTelemetrySpan.java
new file mode 100644
index 00000000000..98e1540e760
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenTelemetrySpan.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 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 io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.StatusCode;
+import java.util.Map;
+
+class OpenTelemetrySpan implements ISpan {
+
+  private final io.opentelemetry.api.trace.Span openTelemetrySpan;
+
+  OpenTelemetrySpan(Span openTelemetrySpan) {
+    this.openTelemetrySpan = openTelemetrySpan;
+  }
+
+  Span getOpenTelemetrySpan() {
+    return openTelemetrySpan;
+  }
+
+  @Override
+  public void addAnnotation(String message, Map<String, Object> attributes) {
+    AttributesBuilder otAttributesBuilder = Attributes.builder();
+    for (Map.Entry<String, Object> entry : attributes.entrySet()) {
+      String key = entry.getKey();
+      Object value = entry.getValue();
+      if (value instanceof String) {
+        otAttributesBuilder.put(key, (String) value);
+      } else if (value instanceof Long) {
+        otAttributesBuilder.put(key, (Long) value);
+      }
+    }
+    openTelemetrySpan.addEvent(message, otAttributesBuilder.build());
+  }
+
+  @Override
+  public void addAnnotation(String message) {
+    openTelemetrySpan.addEvent(message);
+  }
+
+  @Override
+  public void addAnnotation(String message, String key, String value) {
+    openTelemetrySpan.addEvent(message, Attributes.builder().put(key, value).build());
+  }
+
+  @Override
+  public void addAnnotation(String message, String key, long value) {
+    openTelemetrySpan.addEvent(message, Attributes.builder().put(key, value).build());
+  }
+
+  @Override
+  public void addAnnotation(String message, Throwable e) {
+    openTelemetrySpan.addEvent(message, this.createOpenTelemetryExceptionAnnotations(e));
+  }
+
+  @Override
+  public void setStatus(Throwable e) {
+    if (e instanceof SpannerException) {
+      openTelemetrySpan.setStatus(StatusCode.ERROR, ((SpannerException) e).getErrorCode().name());
+    } else {
+      openTelemetrySpan.setStatus(StatusCode.ERROR, ErrorCode.INTERNAL.name());
+    }
+    openTelemetrySpan.recordException(e);
+  }
+
+  @Override
+  public void setStatus(ErrorCode errorCode) {
+    openTelemetrySpan.setStatus(StatusCode.ERROR, errorCode.name());
+  }
+
+  @Override
+  public void end() {
+    openTelemetrySpan.end();
+  }
+
+  private Attributes createOpenTelemetryExceptionAnnotations(Throwable e) {
+    AttributesBuilder attributesBuilder = Attributes.builder();
+    if (e instanceof SpannerException) {
+      attributesBuilder.put("Status", ((SpannerException) e).getErrorCode().toString());
+    }
+    return attributesBuilder.build();
+  }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java
index 36991b18c3d..cabc270566c 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.api.core.InternalApi;
 import com.google.api.gax.grpc.GrpcStatusCode;
 import com.google.api.gax.rpc.AbortedException;
 import com.google.api.gax.rpc.DeadlineExceededException;
@@ -38,7 +39,6 @@
 import com.google.spanner.v1.TransactionOptions;
 import com.google.spanner.v1.TransactionSelector;
 import io.grpc.Status;
-import io.opencensus.trace.Span;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
@@ -46,6 +46,7 @@
 import org.threeten.bp.Duration;
 import org.threeten.bp.temporal.ChronoUnit;
 
+@InternalApi
 public class PartitionedDmlTransaction implements SessionImpl.SessionTransaction {
 
   private static final Logger LOGGER = Logger.getLogger(PartitionedDmlTransaction.class.getName());
@@ -137,9 +138,9 @@ public void invalidate() {
     isValid = false;
   }
 
-  // No-op method needed to implement SessionTransaction interface.
+  /** No-op method needed to implement SessionTransaction interface. */
   @Override
-  public void setSpan(Span span) {}
+  public void setSpan(ISpan span) {}
 
   private Duration tryUpdateTimeout(final Duration timeout, final Stopwatch stopwatch) {
     final Duration remainingTimeout =
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java
index 474d99671e8..b294ef33395 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java
@@ -25,8 +25,6 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
-import io.opencensus.common.Scope;
-import io.opencensus.trace.Span;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -127,16 +125,17 @@ private BatchCreateSessionsRunnable(
     public void run() {
       List<SessionImpl> sessions;
       int remainingSessionsToCreate = sessionCount;
-      Span span = SpannerImpl.tracer.spanBuilder(SpannerImpl.BATCH_CREATE_SESSIONS).startSpan();
-      try (Scope s = SpannerImpl.tracer.withSpan(span)) {
-        SpannerImpl.tracer
+      ISpan span = spanner.getTracer().spanBuilder(SpannerImpl.BATCH_CREATE_SESSIONS);
+      try (IScope s = spanner.getTracer().withSpan(span)) {
+        spanner
+            .getTracer()
             .getCurrentSpan()
             .addAnnotation(String.format("Creating %d sessions", sessionCount));
         while (remainingSessionsToCreate > 0) {
           try {
             sessions = internalBatchCreateSessions(remainingSessionsToCreate, channelHint);
           } catch (Throwable t) {
-            TraceUtil.setWithFailure(SpannerImpl.tracer.getCurrentSpan(), t);
+            spanner.getTracer().getCurrentSpan().setStatus(t);
             consumer.onSessionCreateFailure(t, remainingSessionsToCreate);
             break;
           }
@@ -146,7 +145,7 @@ public void run() {
           remainingSessionsToCreate -= sessions.size();
         }
       } finally {
-        span.end(TraceUtil.END_SPAN_OPTIONS);
+        span.end();
       }
     }
   }
@@ -206,8 +205,8 @@ SessionImpl createSession() {
     synchronized (this) {
       options = optionMap(SessionOption.channelHint(sessionChannelCounter++));
     }
-    Span span = SpannerImpl.tracer.spanBuilder(SpannerImpl.CREATE_SESSION).startSpan();
-    try (Scope s = SpannerImpl.tracer.withSpan(span)) {
+    ISpan span = spanner.getTracer().spanBuilder(SpannerImpl.CREATE_SESSION);
+    try (IScope s = spanner.getTracer().withSpan(span)) {
       com.google.spanner.v1.Session session =
           spanner
               .getRpc()
@@ -218,10 +217,10 @@ SessionImpl createSession() {
                   options);
       return new SessionImpl(spanner, session.getName(), options);
     } catch (RuntimeException e) {
-      TraceUtil.setWithFailure(span, e);
+      span.setStatus(e);
       throw e;
     } finally {
-      span.end(TraceUtil.END_SPAN_OPTIONS);
+      span.end();
     }
   }
 
@@ -290,13 +289,13 @@ void asyncBatchCreateSessions(
   private List<SessionImpl> internalBatchCreateSessions(
       final int sessionCount, final long channelHint) throws SpannerException {
     final Map<SpannerRpc.Option, ?> options = optionMap(SessionOption.channelHint(channelHint));
-    Span parent = SpannerImpl.tracer.getCurrentSpan();
-    Span span =
-        SpannerImpl.tracer
-            .spanBuilderWithExplicitParent(SpannerImpl.BATCH_CREATE_SESSIONS_REQUEST, parent)
-            .startSpan();
+    ISpan parent = spanner.getTracer().getCurrentSpan();
+    ISpan span =
+        spanner
+            .getTracer()
+            .spanBuilderWithExplicitParent(SpannerImpl.BATCH_CREATE_SESSIONS_REQUEST, parent);
     span.addAnnotation(String.format("Requesting %d sessions", sessionCount));
-    try (Scope s = SpannerImpl.tracer.withSpan(span)) {
+    try (IScope s = spanner.getTracer().withSpan(span)) {
       List<com.google.spanner.v1.Session> sessions =
           spanner
               .getRpc()
@@ -309,14 +308,15 @@ private List<SessionImpl> internalBatchCreateSessions(
       span.addAnnotation(
           String.format(
               "Request for %d sessions returned %d sessions", sessionCount, sessions.size()));
-      span.end(TraceUtil.END_SPAN_OPTIONS);
+      span.end();
       List<SessionImpl> res = new ArrayList<>(sessionCount);
       for (com.google.spanner.v1.Session session : sessions) {
         res.add(new SessionImpl(spanner, session.getName(), options));
       }
       return res;
     } catch (RuntimeException e) {
-      TraceUtil.endSpanWithFailure(span, e);
+      span.setStatus(e);
+      span.end();
       throw e;
     }
   }
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java
index 92e75e332e7..29928f61cec 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java
@@ -44,10 +44,6 @@
 import com.google.spanner.v1.RequestOptions;
 import com.google.spanner.v1.Transaction;
 import com.google.spanner.v1.TransactionOptions;
-import io.opencensus.common.Scope;
-import io.opencensus.trace.Span;
-import io.opencensus.trace.Tracer;
-import io.opencensus.trace.Tracing;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -61,8 +57,7 @@
  * users need not be aware of the actual session management, pooling and handling.
  */
 class SessionImpl implements Session {
-
-  private static final Tracer tracer = Tracing.getTracer();
+  private final TraceWrapper tracer;
 
   /** Keep track of running transactions on this session per thread. */
   static final ThreadLocal<Boolean> hasPendingTransaction = ThreadLocal.withInitial(() -> false);
@@ -93,7 +88,7 @@ interface SessionTransaction {
     void invalidate();
 
     /** Registers the current span on the transaction. */
-    void setSpan(Span span);
+    void setSpan(ISpan span);
   }
 
   private final SpannerImpl spanner;
@@ -102,11 +97,12 @@ interface SessionTransaction {
   private SessionTransaction activeTransaction;
   ByteString readyTransactionId;
   private final Map<SpannerRpc.Option, ?> options;
-  private Span currentSpan;
   private volatile Instant lastUseTime;
+  private ISpan currentSpan;
 
   SessionImpl(SpannerImpl spanner, String name, Map<SpannerRpc.Option, ?> options) {
     this.spanner = spanner;
+    this.tracer = spanner.getTracer();
     this.options = options;
     this.name = checkNotNull(name);
     this.databaseId = SessionId.of(name).getDatabaseId();
@@ -122,11 +118,11 @@ public String getName() {
     return options;
   }
 
-  void setCurrentSpan(Span span) {
+  void setCurrentSpan(ISpan span) {
     currentSpan = span;
   }
 
-  Span getCurrentSpan() {
+  ISpan getCurrentSpan() {
     return currentSpan;
   }
 
@@ -202,15 +198,15 @@ public CommitResponse writeAtLeastOnceWithOptions(
       requestBuilder.setRequestOptions(commitRequestOptions);
     }
     CommitRequest request = requestBuilder.build();
-    Span span = tracer.spanBuilder(SpannerImpl.COMMIT).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(SpannerImpl.COMMIT);
+    try (IScope s = tracer.withSpan(span)) {
       return SpannerRetryHelper.runTxWithRetriesOnAborted(
           () -> new CommitResponse(spanner.getRpc().commit(request, this.options)));
     } catch (RuntimeException e) {
-      TraceUtil.setWithFailure(span, e);
+      span.setStatus(e);
       throw e;
     } finally {
-      span.end(TraceUtil.END_SPAN_OPTIONS);
+      span.end();
     }
   }
 
@@ -242,14 +238,14 @@ public ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
     if (batchWriteRequestOptions != null) {
       requestBuilder.setRequestOptions(batchWriteRequestOptions);
     }
-    Span span = tracer.spanBuilder(SpannerImpl.BATCH_WRITE).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(SpannerImpl.BATCH_WRITE);
+    try (IScope s = tracer.withSpan(span)) {
       return spanner.getRpc().batchWriteAtLeastOnce(requestBuilder.build(), this.options);
     } catch (Throwable e) {
-      TraceUtil.setWithFailure(span, e);
+      span.setStatus(e);
       throw SpannerExceptionFactory.newSpannerException(e);
     } finally {
-      span.end(TraceUtil.END_SPAN_OPTIONS);
+      span.end();
     }
   }
 
@@ -269,6 +265,7 @@ public ReadContext singleUse(TimestampBound bound) {
             .setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks())
             .setDefaultDirectedReadOptions(spanner.getOptions().getDirectedReadOptions())
             .setSpan(currentSpan)
+            .setTracer(tracer)
             .setExecutorProvider(spanner.getAsyncExecutorProvider())
             .build());
   }
@@ -289,6 +286,7 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) {
             .setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks())
             .setDefaultDirectedReadOptions(spanner.getOptions().getDirectedReadOptions())
             .setSpan(currentSpan)
+            .setTracer(tracer)
             .setExecutorProvider(spanner.getAsyncExecutorProvider())
             .buildSingleUseReadOnlyTransaction());
   }
@@ -309,6 +307,7 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) {
             .setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks())
             .setDefaultDirectedReadOptions(spanner.getOptions().getDirectedReadOptions())
             .setSpan(currentSpan)
+            .setTracer(tracer)
             .setExecutorProvider(spanner.getAsyncExecutorProvider())
             .build());
   }
@@ -325,7 +324,7 @@ public AsyncRunner runAsync(TransactionOption... options) {
 
   @Override
   public TransactionManager transactionManager(TransactionOption... options) {
-    return new TransactionManagerImpl(this, currentSpan, options);
+    return new TransactionManagerImpl(this, currentSpan, tracer, options);
   }
 
   @Override
@@ -346,14 +345,14 @@ public ApiFuture<Empty> asyncClose() {
 
   @Override
   public void close() {
-    Span span = tracer.spanBuilder(SpannerImpl.DELETE_SESSION).startSpan();
-    try (Scope s = tracer.withSpan(span)) {
+    ISpan span = tracer.spanBuilder(SpannerImpl.DELETE_SESSION);
+    try (IScope s = tracer.withSpan(span)) {
       spanner.getRpc().deleteSession(name, options);
     } catch (RuntimeException e) {
-      TraceUtil.setWithFailure(span, e);
+      span.setStatus(e);
       throw e;
     } finally {
-      span.end(TraceUtil.END_SPAN_OPTIONS);
+      span.end();
     }
   }
 
@@ -373,7 +372,7 @@ ApiFuture<ByteString> beginTransactionAsync(boolean routeToLeader) {
 
   ApiFuture<ByteString> beginTransactionAsync(Options transactionOptions, boolean routeToLeader) {
     final SettableApiFuture<ByteString> res = SettableApiFuture.create();
-    final Span span = tracer.spanBuilder(SpannerImpl.BEGIN_TRANSACTION).startSpan();
+    final ISpan span = tracer.spanBuilder(SpannerImpl.BEGIN_TRANSACTION);
     final BeginTransactionRequest request =
         BeginTransactionRequest.newBuilder()
             .setSession(name)
@@ -382,30 +381,31 @@ ApiFuture<ByteString> beginTransactionAsync(Options transactionOptions, boolean
     final ApiFuture<Transaction> requestFuture =
         spanner.getRpc().beginTransactionAsync(request, options, routeToLeader);
     requestFuture.addListener(
-        tracer.withSpan(
-            span,
-            () -> {
-              try {
-                Transaction txn = requestFuture.get();
-                if (txn.getId().isEmpty()) {
-                  throw newSpannerException(
-                      ErrorCode.INTERNAL, "Missing id in transaction\n" + getName());
-                }
-                span.end(TraceUtil.END_SPAN_OPTIONS);
-                res.set(txn.getId());
-              } catch (ExecutionException e) {
-                TraceUtil.endSpanWithFailure(span, e);
-                res.setException(
-                    SpannerExceptionFactory.newSpannerException(
-                        e.getCause() == null ? e : e.getCause()));
-              } catch (InterruptedException e) {
-                TraceUtil.endSpanWithFailure(span, e);
-                res.setException(SpannerExceptionFactory.propagateInterrupt(e));
-              } catch (Exception e) {
-                TraceUtil.endSpanWithFailure(span, e);
-                res.setException(e);
-              }
-            }),
+        () -> {
+          try (IScope s = tracer.withSpan(span)) {
+            Transaction txn = requestFuture.get();
+            if (txn.getId().isEmpty()) {
+              throw newSpannerException(
+                  ErrorCode.INTERNAL, "Missing id in transaction\n" + getName());
+            }
+            span.end();
+            res.set(txn.getId());
+          } catch (ExecutionException e) {
+            span.setStatus(e);
+            span.end();
+            res.setException(
+                SpannerExceptionFactory.newSpannerException(
+                    e.getCause() == null ? e : e.getCause()));
+          } catch (InterruptedException e) {
+            span.setStatus(e);
+            span.end();
+            res.setException(SpannerExceptionFactory.propagateInterrupt(e));
+          } catch (Exception e) {
+            span.setStatus(e);
+            span.end();
+            res.setException(e);
+          }
+        },
         MoreExecutors.directExecutor());
     return res;
   }
@@ -424,6 +424,7 @@ TransactionContextImpl newTransaction(Options options) {
         .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId))
         .setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks())
         .setSpan(currentSpan)
+        .setTracer(tracer)
         .setExecutorProvider(spanner.getAsyncExecutorProvider())
         .setClock(poolMaintainerClock == null ? new Clock() : poolMaintainerClock)
         .build();
@@ -446,4 +447,8 @@ <T extends SessionTransaction> T setActive(@Nullable T ctx) {
   boolean hasReadyTransaction() {
     return readyTransactionId != null;
   }
+
+  TraceWrapper getTracer() {
+    return tracer;
+  }
 }
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java
index 54a0a292cd8..cc24dd2ba0f 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java
@@ -22,17 +22,21 @@
 import static com.google.cloud.spanner.MetricRegistryConstants.MAX_ALLOWED_SESSIONS_DESCRIPTION;
 import static com.google.cloud.spanner.MetricRegistryConstants.MAX_IN_USE_SESSIONS;
 import static com.google.cloud.spanner.MetricRegistryConstants.MAX_IN_USE_SESSIONS_DESCRIPTION;
+import static com.google.cloud.spanner.MetricRegistryConstants.METRIC_PREFIX;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_ACQUIRED_SESSIONS;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_ACQUIRED_SESSIONS_DESCRIPTION;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_IN_USE_SESSIONS;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_READ_SESSIONS;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_RELEASED_SESSIONS;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_RELEASED_SESSIONS_DESCRIPTION;
+import static com.google.cloud.spanner.MetricRegistryConstants.NUM_SESSIONS_AVAILABLE;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_SESSIONS_BEING_PREPARED;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_SESSIONS_IN_POOL;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_SESSIONS_IN_POOL_DESCRIPTION;
+import static com.google.cloud.spanner.MetricRegistryConstants.NUM_SESSIONS_IN_USE;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_WRITE_SESSIONS;
 import static com.google.cloud.spanner.MetricRegistryConstants.SESSIONS_TIMEOUTS_DESCRIPTION;
+import static com.google.cloud.spanner.MetricRegistryConstants.SESSIONS_TYPE;
 import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_DEFAULT_LABEL_VALUES;
 import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_LABEL_KEYS;
 import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_LABEL_KEYS_WITH_TYPE;
@@ -62,7 +66,6 @@
 import com.google.common.base.Preconditions;
 import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.ForwardingListenableFuture;
 import com.google.common.util.concurrent.ForwardingListenableFuture.SimpleForwardingListenableFuture;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -71,20 +74,16 @@
 import com.google.protobuf.Empty;
 import com.google.spanner.v1.BatchWriteResponse;
 import com.google.spanner.v1.ResultSetStats;
-import io.opencensus.common.Scope;
 import io.opencensus.metrics.DerivedLongCumulative;
 import io.opencensus.metrics.DerivedLongGauge;
 import io.opencensus.metrics.LabelValue;
 import io.opencensus.metrics.MetricOptions;
 import io.opencensus.metrics.MetricRegistry;
 import io.opencensus.metrics.Metrics;
-import io.opencensus.trace.Annotation;
-import io.opencensus.trace.AttributeValue;
-import io.opencensus.trace.BlankSpan;
-import io.opencensus.trace.Span;
-import io.opencensus.trace.Status;
-import io.opencensus.trace.Tracer;
-import io.opencensus.trace.Tracing;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.api.metrics.Meter;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -117,7 +116,7 @@
 class SessionPool {
 
   private static final Logger logger = Logger.getLogger(SessionPool.class.getName());
-  private static final Tracer tracer = Tracing.getTracer();
+  private final TraceWrapper tracer;
   static final String WAIT_FOR_SESSION = "SessionPool.WaitForSession";
 
   /**
@@ -1094,7 +1093,7 @@ private enum SessionState {
   }
 
   private PooledSessionFuture createPooledSessionFuture(
-      ListenableFuture<PooledSession> future, Span span) {
+      ListenableFuture<PooledSession> future, ISpan span) {
     return new PooledSessionFuture(future, span);
   }
 
@@ -1103,10 +1102,10 @@ class PooledSessionFuture extends SimpleForwardingListenableFuture<PooledSession
     private volatile LeakedSessionException leakedException;
     private volatile AtomicBoolean inUse = new AtomicBoolean();
     private volatile CountDownLatch initialized = new CountDownLatch(1);
-    private final Span span;
+    private final ISpan span;
 
     @VisibleForTesting
-    PooledSessionFuture(ListenableFuture<PooledSession> delegate, Span span) {
+    PooledSessionFuture(ListenableFuture<PooledSession> delegate, ISpan span) {
       super(delegate);
       this.span = span;
     }
@@ -1338,7 +1337,7 @@ PooledSession get(final boolean eligibleForLongRunning) {
         }
         if (res != null) {
           res.markBusy(span);
-          span.addAnnotation(sessionAnnotation(res));
+          span.addAnnotation("Using Session", "sessionId", res.getName());
           synchronized (lock) {
             incrementNumSessionsInUse();
             checkedOutSessions.add(this);
@@ -1572,8 +1571,8 @@ public void prepareReadWriteTransaction() {
 
     private void keepAlive() {
       markUsed();
-      final Span previousSpan = delegate.getCurrentSpan();
-      delegate.setCurrentSpan(BlankSpan.INSTANCE);
+      final ISpan previousSpan = delegate.getCurrentSpan();
+      delegate.setCurrentSpan(tracer.getBlankSpan());
       try (ResultSet resultSet =
           delegate
               .singleUse(TimestampBound.ofMaxStaleness(60, TimeUnit.SECONDS))
@@ -1612,7 +1611,7 @@ private Dialect determineDialect() {
       }
     }
 
-    private void markBusy(Span span) {
+    private void markBusy(ISpan span) {
       this.delegate.setCurrentSpan(span);
       this.state = SessionState.BUSY;
     }
@@ -1652,14 +1651,14 @@ private void put(SpannerException e) {
     public PooledSession get() {
       long currentTimeout = options.getInitialWaitForSessionTimeoutMillis();
       while (true) {
-        Span span = tracer.spanBuilder(WAIT_FOR_SESSION).startSpan();
-        try (Scope waitScope = tracer.withSpan(span)) {
+        ISpan span = tracer.spanBuilder(WAIT_FOR_SESSION);
+        try (IScope waitScope = tracer.withSpan(span)) {
           PooledSession s =
               pollUninterruptiblyWithTimeout(currentTimeout, options.getAcquireSessionTimeout());
           if (s == null) {
             // Set the status to DEADLINE_EXCEEDED and retry.
             numWaiterTimeouts.incrementAndGet();
-            tracer.getCurrentSpan().setStatus(Status.DEADLINE_EXCEEDED);
+            tracer.getCurrentSpan().setStatus(ErrorCode.DEADLINE_EXCEEDED);
             currentTimeout = Math.min(currentTimeout * 2, MAX_SESSION_WAIT_TIMEOUT);
           } else {
             return s;
@@ -1668,12 +1667,12 @@ public PooledSession get() {
           if (e instanceof SpannerException
               && ErrorCode.RESOURCE_EXHAUSTED.equals(((SpannerException) e).getErrorCode())) {
             numWaiterTimeouts.incrementAndGet();
-            tracer.getCurrentSpan().setStatus(Status.RESOURCE_EXHAUSTED);
+            tracer.getCurrentSpan().setStatus(ErrorCode.RESOURCE_EXHAUSTED);
           }
-          TraceUtil.setWithFailure(span, e);
+          span.setStatus(e);
           throw e;
         } finally {
-          span.end(TraceUtil.END_SPAN_OPTIONS);
+          span.end();
         }
       }
     }
@@ -2081,7 +2080,11 @@ enum Position {
    * be created.
    */
   static SessionPool createPool(
-      SpannerOptions spannerOptions, SessionClient sessionClient, List<LabelValue> labelValues) {
+      SpannerOptions spannerOptions,
+      SessionClient sessionClient,
+      TraceWrapper tracer,
+      List<LabelValue> labelValues,
+      Attributes attributes) {
     final SessionPoolOptions sessionPoolOptions = spannerOptions.getSessionPoolOptions();
 
     // A clock instance is passed in {@code SessionPoolOptions} in order to allow mocking via tests.
@@ -2094,14 +2097,26 @@ static SessionPool createPool(
         poolMaintainerClock == null ? new Clock() : poolMaintainerClock,
         Position.RANDOM,
         Metrics.getMetricRegistry(),
-        labelValues);
+        tracer,
+        labelValues,
+        spannerOptions.getOpenTelemetry(),
+        attributes);
   }
 
   static SessionPool createPool(
       SessionPoolOptions poolOptions,
       ExecutorFactory<ScheduledExecutorService> executorFactory,
-      SessionClient sessionClient) {
-    return createPool(poolOptions, executorFactory, sessionClient, new Clock(), Position.RANDOM);
+      SessionClient sessionClient,
+      TraceWrapper tracer,
+      OpenTelemetry openTelemetry) {
+    return createPool(
+        poolOptions,
+        executorFactory,
+        sessionClient,
+        new Clock(),
+        Position.RANDOM,
+        tracer,
+        openTelemetry);
   }
 
   static SessionPool createPool(
@@ -2109,7 +2124,9 @@ static SessionPool createPool(
       ExecutorFactory<ScheduledExecutorService> executorFactory,
       SessionClient sessionClient,
       Clock clock,
-      Position initialReleasePosition) {
+      Position initialReleasePosition,
+      TraceWrapper tracer,
+      OpenTelemetry openTelemetry) {
     return createPool(
         poolOptions,
         null,
@@ -2118,7 +2135,10 @@ static SessionPool createPool(
         clock,
         initialReleasePosition,
         Metrics.getMetricRegistry(),
-        SPANNER_DEFAULT_LABEL_VALUES);
+        tracer,
+        SPANNER_DEFAULT_LABEL_VALUES,
+        openTelemetry,
+        null);
   }
 
   static SessionPool createPool(
@@ -2129,7 +2149,10 @@ static SessionPool createPool(
       Clock clock,
       Position initialReleasePosition,
       MetricRegistry metricRegistry,
-      List<LabelValue> labelValues) {
+      TraceWrapper tracer,
+      List<LabelValue> labelValues,
+      OpenTelemetry openTelemetry,
+      Attributes attributes) {
     SessionPool pool =
         new SessionPool(
             poolOptions,
@@ -2140,7 +2163,10 @@ static SessionPool createPool(
             clock,
             initialReleasePosition,
             metricRegistry,
-            labelValues);
+            tracer,
+            labelValues,
+            openTelemetry,
+            attributes);
     pool.initPool();
     return pool;
   }
@@ -2154,7 +2180,10 @@ private SessionPool(
       Clock clock,
       Position initialReleasePosition,
       MetricRegistry metricRegistry,
-      List<LabelValue> labelValues) {
+      TraceWrapper tracer,
+      List<LabelValue> labelValues,
+      OpenTelemetry openTelemetry,
+      Attributes attributes) {
     this.options = options;
     this.databaseRole = databaseRole;
     this.executorFactory = executorFactory;
@@ -2163,7 +2192,9 @@ private SessionPool(
     this.clock = clock;
     this.initialReleasePosition = initialReleasePosition;
     this.poolMaintainer = new PoolMaintainer();
-    this.initMetricsCollection(metricRegistry, labelValues);
+    this.tracer = tracer;
+    this.initOpenCensusMetricsCollection(metricRegistry, labelValues);
+    this.initOpenTelemetryMetricsCollection(openTelemetry, attributes);
     this.waitOnMinSessionsLatch =
         options.getMinSessions() > 0 ? new CountDownLatch(1) : new CountDownLatch(0);
   }
@@ -2351,7 +2382,7 @@ boolean isValid() {
    * </ol>
    */
   PooledSessionFuture getSession() throws SpannerException {
-    Span span = Tracing.getTracer().getCurrentSpan();
+    ISpan span = tracer.getCurrentSpan();
     span.addAnnotation("Acquiring session");
     WaiterFuture waiter = null;
     PooledSession sess = null;
@@ -2383,7 +2414,7 @@ PooledSessionFuture getSession() throws SpannerException {
   }
 
   private PooledSessionFuture checkoutSession(
-      final Span span, final PooledSession readySession, WaiterFuture waiter) {
+      final ISpan span, final PooledSession readySession, WaiterFuture waiter) {
     ListenableFuture<PooledSession> sessionFuture;
     if (waiter != null) {
       logger.log(
@@ -2416,12 +2447,6 @@ PooledSessionFuture replaceSession(SessionNotFoundException e, PooledSessionFutu
     }
   }
 
-  private Annotation sessionAnnotation(Session session) {
-    AttributeValue sessionId = AttributeValue.stringAttributeValue(session.getName());
-    return Annotation.fromDescriptionAndAttributes(
-        "Using Session", ImmutableMap.of("sessionId", sessionId));
-  }
-
   private void incrementNumSessionsInUse() {
     synchronized (lock) {
       if (maxSessionsInUse < ++numSessionsInUse) {
@@ -2432,7 +2457,7 @@ private void incrementNumSessionsInUse() {
   }
 
   private void maybeCreateSession() {
-    Span span = Tracing.getTracer().getCurrentSpan();
+    ISpan span = tracer.getCurrentSpan();
     synchronized (lock) {
       if (numWaiters() >= numSessionsBeingCreated) {
         if (canCreateSession()) {
@@ -2799,13 +2824,17 @@ public void onSessionCreateFailure(Throwable t, int createFailureForSessionCount
   }
 
   /**
-   * Initializes and creates Spanner session relevant metrics. When coupled with an exporter, it
-   * allows users to monitor client behavior.
+   * Initializes and creates Spanner session relevant metrics using OpenCensus. When coupled with an
+   * exporter, it allows users to monitor client behavior.
    */
-  private void initMetricsCollection(MetricRegistry metricRegistry, List<LabelValue> labelValues) {
+  private void initOpenCensusMetricsCollection(
+      MetricRegistry metricRegistry, List<LabelValue> labelValues) {
+    if (!SpannerOptions.isEnabledOpenCensusMetrics()) {
+      return;
+    }
     DerivedLongGauge maxInUseSessionsMetric =
         metricRegistry.addDerivedLongGauge(
-            MAX_IN_USE_SESSIONS,
+            METRIC_PREFIX + MAX_IN_USE_SESSIONS,
             MetricOptions.builder()
                 .setDescription(MAX_IN_USE_SESSIONS_DESCRIPTION)
                 .setUnit(COUNT)
@@ -2814,7 +2843,7 @@ private void initMetricsCollection(MetricRegistry metricRegistry, List<LabelValu
 
     DerivedLongGauge maxAllowedSessionsMetric =
         metricRegistry.addDerivedLongGauge(
-            MAX_ALLOWED_SESSIONS,
+            METRIC_PREFIX + MAX_ALLOWED_SESSIONS,
             MetricOptions.builder()
                 .setDescription(MAX_ALLOWED_SESSIONS_DESCRIPTION)
                 .setUnit(COUNT)
@@ -2823,7 +2852,7 @@ private void initMetricsCollection(MetricRegistry metricRegistry, List<LabelValu
 
     DerivedLongCumulative sessionsTimeouts =
         metricRegistry.addDerivedLongCumulative(
-            GET_SESSION_TIMEOUTS,
+            METRIC_PREFIX + GET_SESSION_TIMEOUTS,
             MetricOptions.builder()
                 .setDescription(SESSIONS_TIMEOUTS_DESCRIPTION)
                 .setUnit(COUNT)
@@ -2832,7 +2861,7 @@ private void initMetricsCollection(MetricRegistry metricRegistry, List<LabelValu
 
     DerivedLongCumulative numAcquiredSessionsMetric =
         metricRegistry.addDerivedLongCumulative(
-            NUM_ACQUIRED_SESSIONS,
+            METRIC_PREFIX + NUM_ACQUIRED_SESSIONS,
             MetricOptions.builder()
                 .setDescription(NUM_ACQUIRED_SESSIONS_DESCRIPTION)
                 .setUnit(COUNT)
@@ -2841,7 +2870,7 @@ private void initMetricsCollection(MetricRegistry metricRegistry, List<LabelValu
 
     DerivedLongCumulative numReleasedSessionsMetric =
         metricRegistry.addDerivedLongCumulative(
-            NUM_RELEASED_SESSIONS,
+            METRIC_PREFIX + NUM_RELEASED_SESSIONS,
             MetricOptions.builder()
                 .setDescription(NUM_RELEASED_SESSIONS_DESCRIPTION)
                 .setUnit(COUNT)
@@ -2850,7 +2879,7 @@ private void initMetricsCollection(MetricRegistry metricRegistry, List<LabelValu
 
     DerivedLongGauge numSessionsInPoolMetric =
         metricRegistry.addDerivedLongGauge(
-            NUM_SESSIONS_IN_POOL,
+            METRIC_PREFIX + NUM_SESSIONS_IN_POOL,
             MetricOptions.builder()
                 .setDescription(NUM_SESSIONS_IN_POOL_DESCRIPTION)
                 .setUnit(COUNT)
@@ -2912,4 +2941,83 @@ private void initMetricsCollection(MetricRegistry metricRegistry, List<LabelValu
         // TODO: Remove metric.
         ignored -> 0L);
   }
+
+  /**
+   * Initializes and creates Spanner session relevant metrics using OpenTelemetry. When coupled with
+   * an exporter, it allows users to monitor client behavior.
+   */
+  private void initOpenTelemetryMetricsCollection(
+      OpenTelemetry openTelemetry, Attributes attributes) {
+    if (openTelemetry == null || !SpannerOptions.isEnabledOpenTelemetryMetrics()) {
+      return;
+    }
+
+    Meter meter = openTelemetry.getMeter(MetricRegistryConstants.INSTRUMENTATION_SCOPE);
+    meter
+        .gaugeBuilder(MAX_ALLOWED_SESSIONS)
+        .setDescription(MAX_ALLOWED_SESSIONS_DESCRIPTION)
+        .setUnit(COUNT)
+        .buildWithCallback(
+            measurement -> {
+              // Although Max sessions is a constant value, OpenTelemetry requires to define this as
+              // a callback.
+              measurement.record(options.getMaxSessions(), attributes);
+            });
+
+    meter
+        .gaugeBuilder(MAX_IN_USE_SESSIONS)
+        .setDescription(MAX_IN_USE_SESSIONS_DESCRIPTION)
+        .setUnit(COUNT)
+        .buildWithCallback(
+            measurement -> {
+              measurement.record(this.maxSessionsInUse, attributes);
+            });
+
+    AttributesBuilder attributesBuilder;
+    if (attributes != null) {
+      attributesBuilder = attributes.toBuilder();
+    } else {
+      attributesBuilder = Attributes.builder();
+    }
+    Attributes attributesInUseSessions =
+        attributesBuilder.put(SESSIONS_TYPE, NUM_SESSIONS_IN_USE).build();
+    Attributes attributesAvailableSessions =
+        attributesBuilder.put(SESSIONS_TYPE, NUM_SESSIONS_AVAILABLE).build();
+    meter
+        .upDownCounterBuilder(NUM_SESSIONS_IN_POOL)
+        .setDescription(NUM_SESSIONS_IN_POOL_DESCRIPTION)
+        .setUnit(COUNT)
+        .buildWithCallback(
+            measurement -> {
+              measurement.record(this.numSessionsInUse, attributesInUseSessions);
+              measurement.record(this.sessions.size(), attributesAvailableSessions);
+            });
+
+    meter
+        .counterBuilder(GET_SESSION_TIMEOUTS)
+        .setDescription(SESSIONS_TIMEOUTS_DESCRIPTION)
+        .setUnit(COUNT)
+        .buildWithCallback(
+            measurement -> {
+              measurement.record(this.getNumWaiterTimeouts(), attributes);
+            });
+
+    meter
+        .counterBuilder(NUM_ACQUIRED_SESSIONS)
+        .setDescription(NUM_ACQUIRED_SESSIONS_DESCRIPTION)
+        .setUnit(COUNT)
+        .buildWithCallback(
+            measurement -> {
+              measurement.record(this.numSessionsAcquired, attributes);
+            });
+
+    meter
+        .counterBuilder(NUM_RELEASED_SESSIONS)
+        .setDescription(NUM_RELEASED_SESSIONS_DESCRIPTION)
+        .setUnit(COUNT)
+        .buildWithCallback(
+            measurement -> {
+              measurement.record(this.numSessionsReleased, attributes);
+            });
+  }
 }
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java
index f1a7888c62c..326a51d803e 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java
@@ -37,8 +37,9 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
 import io.opencensus.metrics.LabelValue;
-import io.opencensus.trace.Tracer;
 import io.opencensus.trace.Tracing;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -55,7 +56,14 @@
 /** Default implementation of the Cloud Spanner interface. */
 class SpannerImpl extends BaseService<SpannerOptions> implements Spanner {
   private static final Logger logger = Logger.getLogger(SpannerImpl.class.getName());
-  static final Tracer tracer = Tracing.getTracer();
+  final TraceWrapper tracer =
+      new TraceWrapper(
+          Tracing.getTracer(),
+          this.getOptions()
+              .getOpenTelemetry()
+              .getTracer(
+                  MetricRegistryConstants.INSTRUMENTATION_SCOPE,
+                  GaxProperties.getLibraryVersion(this.getOptions().getClass())));
 
   static final String CREATE_SESSION = "CloudSpannerOperation.CreateSession";
   static final String BATCH_CREATE_SESSIONS = "CloudSpannerOperation.BatchCreateSessions";
@@ -148,6 +156,10 @@ QueryOptions getDefaultQueryOptions(DatabaseId databaseId) {
     return getOptions().getDefaultQueryOptions(databaseId);
   }
 
+  TraceWrapper getTracer() {
+    return this.tracer;
+  }
+
   /**
    * Returns the {@link ExecutorProvider} to use for async methods that need a background executor.
    */
@@ -219,9 +231,19 @@ public DatabaseClient getDatabaseClient(DatabaseId db) {
                 LabelValue.create(db.getDatabase()),
                 LabelValue.create(db.getInstanceId().getName()),
                 LabelValue.create(GaxProperties.getLibraryVersion(getOptions().getClass())));
+
+        AttributesBuilder attributesBuilder = Attributes.builder();
+        attributesBuilder.put("client_id", clientId);
+        attributesBuilder.put("database", db.getDatabase());
+        attributesBuilder.put("instance_id", db.getInstanceId().getName());
+
         SessionPool pool =
             SessionPool.createPool(
-                getOptions(), SpannerImpl.this.getSessionClient(db), labelValues);
+                getOptions(),
+                SpannerImpl.this.getSessionClient(db),
+                this.tracer,
+                labelValues,
+                attributesBuilder.build());
         pool.maybeWaitOnMinSessions();
         DatabaseClientImpl dbClient = createDatabaseClient(clientId, pool);
         dbClients.put(db, dbClient);
@@ -232,7 +254,7 @@ public DatabaseClient getDatabaseClient(DatabaseId db) {
 
   @VisibleForTesting
   DatabaseClientImpl createDatabaseClient(String clientId, SessionPool pool) {
-    return new DatabaseClientImpl(clientId, pool);
+    return new DatabaseClientImpl(clientId, pool, tracer);
   }
 
   @Override
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 0d9f3b85c19..9c6044aa938 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
@@ -19,6 +19,7 @@
 import com.google.api.core.ApiFunction;
 import com.google.api.core.BetaApi;
 import com.google.api.core.InternalApi;
+import com.google.api.core.ObsoleteApi;
 import com.google.api.gax.core.ExecutorProvider;
 import com.google.api.gax.grpc.GrpcCallContext;
 import com.google.api.gax.grpc.GrpcInterceptorProvider;
@@ -61,6 +62,8 @@
 import io.grpc.ExperimentalApi;
 import io.grpc.ManagedChannelBuilder;
 import io.grpc.MethodDescriptor;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -75,12 +78,15 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
 import org.threeten.bp.Duration;
 
 /** Options for the Cloud Spanner service. */
 public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {
   private static final long serialVersionUID = 2789571558532701170L;
   private static SpannerEnvironment environment = SpannerEnvironmentImpl.INSTANCE;
+  private static boolean enableOpenCensusMetrics = true;
+  private static boolean enableOpenTelemetryMetrics = false;
 
   private static final String JDBC_API_CLIENT_LIB_TOKEN = "sp-jdbc";
   private static final String HIBERNATE_API_CLIENT_LIB_TOKEN = "sp-hib";
@@ -141,6 +147,17 @@ public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {
   private final boolean attemptDirectPath;
   private final DirectedReadOptions directedReadOptions;
   private final boolean useVirtualThreads;
+  private final OpenTelemetry openTelemetry;
+
+  enum TracingFramework {
+    OPEN_CENSUS,
+    OPEN_TELEMETRY
+  }
+
+  private static final Object lock = new Object();
+
+  @GuardedBy("lock")
+  private static TracingFramework activeTracingFramework;
 
   /** Interface that can be used to provide {@link CallCredentials} to {@link SpannerOptions}. */
   public interface CallCredentialsProvider {
@@ -633,6 +650,7 @@ protected SpannerOptions(Builder builder) {
     attemptDirectPath = builder.attemptDirectPath;
     directedReadOptions = builder.directedReadOptions;
     useVirtualThreads = builder.useVirtualThreads;
+    openTelemetry = builder.openTelemetry;
   }
 
   /**
@@ -737,6 +755,7 @@ public static class Builder
     private boolean attemptDirectPath = true;
     private DirectedReadOptions directedReadOptions;
     private boolean useVirtualThreads = false;
+    private OpenTelemetry openTelemetry;
 
     private static String createCustomClientLibToken(String token) {
       return token + " " + ServiceOptions.getGoogApiClientLibName();
@@ -1243,6 +1262,15 @@ public Builder setEmulatorHost(String emulatorHost) {
       return this;
     }
 
+    /**
+     * Sets OpenTelemetry object to be used for Spanner Metrics and Traces. GlobalOpenTelemetry will
+     * be used as fallback if this options is not set.
+     */
+    public Builder setOpenTelemetry(OpenTelemetry openTelemetry) {
+      this.openTelemetry = openTelemetry;
+      return this;
+    }
+
     /**
      * Enable leader aware routing. Leader aware routing would route all requests in RW/PDML
      * transactions to the leader region.
@@ -1297,6 +1325,11 @@ public SpannerOptions build() {
             this.grpcGcpExtensionEnabled ? GRPC_GCP_ENABLED_DEFAULT_CHANNELS : DEFAULT_CHANNELS;
       }
 
+      synchronized (lock) {
+        if (activeTracingFramework == null) {
+          activeTracingFramework = TracingFramework.OPEN_CENSUS;
+        }
+      }
       return new SpannerOptions(this);
     }
   }
@@ -1326,6 +1359,77 @@ public static void useDefaultEnvironment() {
     SpannerOptions.environment = SpannerEnvironmentImpl.INSTANCE;
   }
 
+  /**
+   * Enables OpenTelemetry traces. Enabling OpenTelemetry traces will disable OpenCensus traces. By
+   * default, OpenCensus traces are enabled.
+   */
+  public static void enableOpenTelemetryTraces() {
+    synchronized (lock) {
+      if (activeTracingFramework != null
+          && activeTracingFramework != TracingFramework.OPEN_TELEMETRY) {
+        throw new IllegalStateException(
+            "ActiveTracingFramework is set to OpenCensus and cannot be reset after SpannerOptions object is created.");
+      }
+      activeTracingFramework = TracingFramework.OPEN_TELEMETRY;
+    }
+  }
+
+  /** Enables OpenCensus traces. Enabling OpenCensus traces will disable OpenTelemetry traces. */
+  @ObsoleteApi(
+      "The OpenCensus project is deprecated. Use enableOpenTelemetryTraces to switch to OpenTelemetry traces")
+  public static void enableOpenCensusTraces() {
+    synchronized (lock) {
+      if (activeTracingFramework != null
+          && activeTracingFramework != TracingFramework.OPEN_CENSUS) {
+        throw new IllegalStateException(
+            "ActiveTracingFramework is set to OpenTelemetry and cannot be reset after SpannerOptions object is created.");
+      }
+      activeTracingFramework = TracingFramework.OPEN_CENSUS;
+    }
+  }
+
+  /**
+   * Always resets the activeTracingFramework. This variable is used for internal testing, and is
+   * not a valid production scenario
+   */
+  @ObsoleteApi(
+      "The OpenCensus project is deprecated. Use enableOpenTelemetryTraces to switch to OpenTelemetry traces")
+  static void resetActiveTracingFramework() {
+    activeTracingFramework = null;
+  }
+
+  public static TracingFramework getActiveTracingFramework() {
+    synchronized (lock) {
+      if (activeTracingFramework == null) {
+        return TracingFramework.OPEN_CENSUS;
+      }
+      return activeTracingFramework;
+    }
+  }
+
+  /** Disables OpenCensus metrics. Disable OpenCensus metrics before creating Spanner client. */
+  public static void disableOpenCensusMetrics() {
+    SpannerOptions.enableOpenCensusMetrics = false;
+  }
+
+  @VisibleForTesting
+  static void enableOpenCensusMetrics() {
+    SpannerOptions.enableOpenCensusMetrics = true;
+  }
+
+  public static boolean isEnabledOpenCensusMetrics() {
+    return SpannerOptions.enableOpenCensusMetrics;
+  }
+
+  /** Enables OpenTelemetry metrics. Enable OpenTelemetry metrics before creating Spanner client. */
+  public static void enableOpenTelemetryMetrics() {
+    SpannerOptions.enableOpenTelemetryMetrics = true;
+  }
+
+  public static boolean isEnabledOpenTelemetryMetrics() {
+    return SpannerOptions.enableOpenTelemetryMetrics;
+  }
+
   @Override
   protected String getDefaultProject() {
     String projectId = getDefaultProjectId();
@@ -1426,6 +1530,18 @@ public boolean isAttemptDirectPath() {
     return attemptDirectPath;
   }
 
+  /**
+   * Returns an instance of OpenTelemetry. If OpenTelemetry object is not set via SpannerOptions
+   * then GlobalOpenTelemetry will be used as fallback.
+   */
+  public OpenTelemetry getOpenTelemetry() {
+    if (this.openTelemetry != null) {
+      return this.openTelemetry;
+    } else {
+      return GlobalOpenTelemetry.get();
+    }
+  }
+
   @BetaApi
   public boolean isUseVirtualThreads() {
     return useVirtualThreads;
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerRpcMetrics.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerRpcMetrics.java
new file mode 100644
index 00000000000..794c211971d
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerRpcMetrics.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 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
+ *
+ *     https://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.api.OpenTelemetry;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.LongHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.util.Arrays;
+import java.util.List;
+
+@InternalApi
+public class SpannerRpcMetrics {
+  private final LongHistogram gfeLatencies;
+  private final LongCounter gfeHeaderMissingCount;
+
+  public SpannerRpcMetrics(OpenTelemetry openTelemetry) {
+    if (!SpannerOptions.isEnabledOpenTelemetryMetrics()) {
+      gfeLatencies = null;
+      gfeHeaderMissingCount = null;
+      return;
+    }
+
+    Meter meter = openTelemetry.getMeter(MetricRegistryConstants.INSTRUMENTATION_SCOPE);
+    List<Long> RPC_MILLIS_BUCKET_BOUNDARIES =
+        Arrays.asList(
+            1L, 2L, 3L, 4L, 5L, 6L, 8L, 10L, 13L, 16L, 20L, 25L, 30L, 40L, 50L, 65L, 80L, 100L,
+            130L, 160L, 200L, 250L, 300L, 400L, 500L, 650L, 800L, 1000L, 2000L, 5000L, 10000L,
+            20000L, 50000L, 100000L);
+    gfeLatencies =
+        meter
+            .histogramBuilder(MetricRegistryConstants.SPANNER_GFE_LATENCY)
+            .ofLongs()
+            .setDescription(MetricRegistryConstants.SPANNER_GFE_LATENCY_DESCRIPTION)
+            .setUnit("ms")
+            .setExplicitBucketBoundariesAdvice(RPC_MILLIS_BUCKET_BOUNDARIES)
+            .build();
+    gfeHeaderMissingCount =
+        meter
+            .counterBuilder(MetricRegistryConstants.SPANNER_GFE_HEADER_MISSING_COUNT)
+            .setDescription(MetricRegistryConstants.SPANNER_GFE_HEADER_MISSING_COUNT_DESCRIPTION)
+            .setUnit(MetricRegistryConstants.COUNT)
+            .build();
+  }
+
+  @InternalApi
+  public void recordGfeLatency(long value, Attributes attributes) {
+    if (gfeLatencies != null) {
+      gfeLatencies.record(value, attributes);
+    }
+  }
+
+  @InternalApi
+  public void recordGfeHeaderMissingCount(long value, Attributes attributes) {
+    if (gfeHeaderMissingCount != null) {
+      gfeHeaderMissingCount.add(value, attributes);
+    }
+  }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceUtil.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceUtil.java
deleted file mode 100644
index 0d429661ad2..00000000000
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceUtil.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 2017 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.cloud.Timestamp;
-import com.google.common.collect.ImmutableMap;
-import com.google.spanner.v1.Transaction;
-import io.opencensus.contrib.grpc.util.StatusConverter;
-import io.opencensus.trace.AttributeValue;
-import io.opencensus.trace.EndSpanOptions;
-import io.opencensus.trace.Span;
-import io.opencensus.trace.Status;
-import java.util.Map;
-
-/** Utility methods for tracing. */
-class TraceUtil {
-
-  static final EndSpanOptions END_SPAN_OPTIONS =
-      EndSpanOptions.builder().setSampleToLocalSpanStore(true).build();
-
-  static Map<String, AttributeValue> getTransactionAnnotations(Transaction t) {
-    return ImmutableMap.of(
-        "Id",
-        AttributeValue.stringAttributeValue(t.getId().toStringUtf8()),
-        "Timestamp",
-        AttributeValue.stringAttributeValue(Timestamp.fromProto(t.getReadTimestamp()).toString()));
-  }
-
-  static ImmutableMap<String, AttributeValue> getExceptionAnnotations(Throwable e) {
-    if (e instanceof SpannerException) {
-      return ImmutableMap.of(
-          "Status",
-          AttributeValue.stringAttributeValue(((SpannerException) e).getErrorCode().toString()));
-    }
-    return ImmutableMap.of();
-  }
-
-  static ImmutableMap<String, AttributeValue> getExceptionAnnotations(SpannerException e) {
-    return ImmutableMap.of(
-        "Status", AttributeValue.stringAttributeValue(e.getErrorCode().toString()));
-  }
-
-  static void setWithFailure(Span span, Throwable e) {
-    if (e instanceof SpannerException) {
-      span.setStatus(
-          StatusConverter.fromGrpcStatus(((SpannerException) e).getErrorCode().getGrpcStatus())
-              .withDescription(e.getMessage()));
-    } else {
-      span.setStatus(Status.INTERNAL.withDescription(e.getMessage()));
-    }
-  }
-
-  static void endSpanWithFailure(Span span, Throwable e) {
-    if (e instanceof SpannerException) {
-      endSpanWithFailure(span, (SpannerException) e);
-    } else {
-      span.setStatus(Status.INTERNAL.withDescription(e.getMessage()));
-      span.end(END_SPAN_OPTIONS);
-    }
-  }
-
-  static void endSpanWithFailure(Span span, SpannerException e) {
-    span.setStatus(
-        StatusConverter.fromGrpcStatus(e.getErrorCode().getGrpcStatus())
-            .withDescription(e.getMessage()));
-    span.end(END_SPAN_OPTIONS);
-  }
-}
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
new file mode 100644
index 00000000000..25796968e9e
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2023 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.cloud.spanner.SpannerOptions.TracingFramework;
+import io.opencensus.trace.BlankSpan;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opentelemetry.context.Context;
+
+class TraceWrapper {
+
+  private final Tracer openCensusTracer;
+  private final io.opentelemetry.api.trace.Tracer openTelemetryTracer;
+
+  TraceWrapper(Tracer openCensusTracer, io.opentelemetry.api.trace.Tracer openTelemetryTracer) {
+    this.openTelemetryTracer = openTelemetryTracer;
+    this.openCensusTracer = openCensusTracer;
+  }
+
+  ISpan spanBuilder(String spanName) {
+    if (SpannerOptions.getActiveTracingFramework().equals(TracingFramework.OPEN_TELEMETRY)) {
+      return new OpenTelemetrySpan(openTelemetryTracer.spanBuilder(spanName).startSpan());
+    } else {
+      return new OpenCensusSpan(openCensusTracer.spanBuilder(spanName).startSpan());
+    }
+  }
+
+  ISpan spanBuilderWithExplicitParent(String spanName, ISpan parentSpan) {
+    if (SpannerOptions.getActiveTracingFramework().equals(TracingFramework.OPEN_TELEMETRY)) {
+      OpenTelemetrySpan otParentSpan = (OpenTelemetrySpan) parentSpan;
+
+      io.opentelemetry.api.trace.Span otSpan;
+
+      if (otParentSpan != null && otParentSpan.getOpenTelemetrySpan() != null) {
+        otSpan =
+            openTelemetryTracer
+                .spanBuilder(spanName)
+                .setParent(Context.current().with(otParentSpan.getOpenTelemetrySpan()))
+                .startSpan();
+      } else {
+        otSpan = openTelemetryTracer.spanBuilder(spanName).startSpan();
+      }
+
+      return new OpenTelemetrySpan(otSpan);
+
+    } else {
+      OpenCensusSpan parentOcSpan = (OpenCensusSpan) parentSpan;
+      Span ocSpan =
+          openCensusTracer
+              .spanBuilderWithExplicitParent(
+                  spanName, parentOcSpan != null ? parentOcSpan.getOpenCensusSpan() : null)
+              .startSpan();
+
+      return new OpenCensusSpan(ocSpan);
+    }
+  }
+
+  ISpan getCurrentSpan() {
+    if (SpannerOptions.getActiveTracingFramework().equals(TracingFramework.OPEN_TELEMETRY)) {
+      return new OpenTelemetrySpan(
+          io.opentelemetry.api.trace.Span.fromContext(io.opentelemetry.context.Context.current()));
+    } else {
+      return new OpenCensusSpan(openCensusTracer.getCurrentSpan());
+    }
+  }
+
+  ISpan getBlankSpan() {
+    if (SpannerOptions.getActiveTracingFramework().equals(TracingFramework.OPEN_TELEMETRY)) {
+      return new OpenTelemetrySpan(io.opentelemetry.api.trace.Span.getInvalid());
+    } else {
+      return new OpenCensusSpan(BlankSpan.INSTANCE);
+    }
+  }
+
+  IScope withSpan(ISpan span) {
+    if (SpannerOptions.getActiveTracingFramework().equals(TracingFramework.OPEN_TELEMETRY)) {
+      OpenTelemetrySpan openTelemetrySpan;
+      if (!(span instanceof OpenTelemetrySpan)) {
+        openTelemetrySpan = new OpenTelemetrySpan(null);
+      } else {
+        openTelemetrySpan = (OpenTelemetrySpan) span;
+      }
+      return new OpenTelemetryScope(openTelemetrySpan.getOpenTelemetrySpan().makeCurrent());
+    } else {
+      OpenCensusSpan openCensusSpan;
+      if (!(span instanceof OpenCensusSpan)) {
+        openCensusSpan = new OpenCensusSpan(null);
+      } else {
+        openCensusSpan = (OpenCensusSpan) span;
+      }
+      return new OpenCensusScope(openCensusTracer.withSpan(openCensusSpan.getOpenCensusSpan()));
+    }
+  }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java
index 2d47fcd5c78..95ffd1168b2 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java
@@ -20,41 +20,39 @@
 import com.google.cloud.spanner.Options.TransactionOption;
 import com.google.cloud.spanner.SessionImpl.SessionTransaction;
 import com.google.common.base.Preconditions;
-import io.opencensus.common.Scope;
-import io.opencensus.trace.Span;
-import io.opencensus.trace.Tracer;
-import io.opencensus.trace.Tracing;
 
 /** Implementation of {@link TransactionManager}. */
 final class TransactionManagerImpl implements TransactionManager, SessionTransaction {
-  private static final Tracer tracer = Tracing.getTracer();
+  private final TraceWrapper tracer;
 
   private final SessionImpl session;
-  private Span span;
+  private ISpan span;
   private final Options options;
 
   private TransactionRunnerImpl.TransactionContextImpl txn;
   private TransactionState txnState;
 
-  TransactionManagerImpl(SessionImpl session, Span span, TransactionOption... options) {
+  TransactionManagerImpl(
+      SessionImpl session, ISpan span, TraceWrapper tracer, TransactionOption... options) {
     this.session = session;
     this.span = span;
+    this.tracer = tracer;
     this.options = Options.fromTransactionOptions(options);
   }
 
-  Span getSpan() {
+  ISpan getSpan() {
     return span;
   }
 
   @Override
-  public void setSpan(Span span) {
+  public void setSpan(ISpan span) {
     this.span = span;
   }
 
   @Override
   public TransactionContext begin() {
     Preconditions.checkState(txn == null, "begin can only be called once");
-    try (Scope s = tracer.withSpan(span)) {
+    try (IScope s = tracer.withSpan(span)) {
       txn = session.newTransaction(options);
       session.setActive(this);
       txnState = TransactionState.STARTED;
@@ -102,7 +100,7 @@ public TransactionContext resetForRetry() {
       throw new IllegalStateException(
           "resetForRetry can only be called if the previous attempt" + " aborted");
     }
-    try (Scope s = tracer.withSpan(span)) {
+    try (IScope s = tracer.withSpan(span)) {
       boolean useInlinedBegin = txn.transactionId != null;
       txn = session.newTransaction(options);
       if (!useInlinedBegin) {
@@ -137,7 +135,7 @@ public void close() {
         txnState = TransactionState.ROLLED_BACK;
       }
     } finally {
-      span.end(TraceUtil.END_SPAN_OPTIONS);
+      span.end();
     }
   }
 
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java
index 5a0cd3618e8..3249be1bdb3 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java
@@ -49,11 +49,6 @@
 import com.google.spanner.v1.Transaction;
 import com.google.spanner.v1.TransactionOptions;
 import com.google.spanner.v1.TransactionSelector;
-import io.opencensus.common.Scope;
-import io.opencensus.trace.AttributeValue;
-import io.opencensus.trace.Span;
-import io.opencensus.trace.Tracer;
-import io.opencensus.trace.Tracing;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Queue;
@@ -71,8 +66,6 @@
 
 /** Default implementation of {@link TransactionRunner}. */
 class TransactionRunnerImpl implements SessionTransaction, TransactionRunner {
-
-  private static final Tracer tracer = Tracing.getTracer();
   private static final Logger txnLogger = Logger.getLogger(TransactionRunner.class.getName());
   /**
    * (Part of) the error message that is returned by Cloud Spanner if a transaction is cancelled
@@ -257,10 +250,7 @@ ApiFuture<Void> ensureTxnAsync() {
       if (transactionId == null || isAborted()) {
         createTxnAsync(res);
       } else {
-        span.addAnnotation(
-            "Transaction Initialized",
-            ImmutableMap.of(
-                "Id", AttributeValue.stringAttributeValue(transactionId.toStringUtf8())));
+        span.addAnnotation("Transaction Initialized", "Id", transactionId.toStringUtf8());
         txnLogger.log(
             Level.FINER,
             "Using prepared transaction {0}",
@@ -277,10 +267,7 @@ private void createTxnAsync(final SettableApiFuture<Void> res) {
           () -> {
             try {
               transactionId = fut.get();
-              span.addAnnotation(
-                  "Transaction Creation Done",
-                  ImmutableMap.of(
-                      "Id", AttributeValue.stringAttributeValue(transactionId.toStringUtf8())));
+              span.addAnnotation("Transaction Creation Done", "Id", transactionId.toStringUtf8());
               txnLogger.log(
                   Level.FINER,
                   "Started transaction {0}",
@@ -288,8 +275,7 @@ private void createTxnAsync(final SettableApiFuture<Void> res) {
               res.set(null);
             } catch (ExecutionException e) {
               span.addAnnotation(
-                  "Transaction Creation Failed",
-                  TraceUtil.getExceptionAnnotations(e.getCause() == null ? e : e.getCause()));
+                  "Transaction Creation Failed", e.getCause() == null ? e : e.getCause());
               res.setException(e.getCause() == null ? e : e.getCause());
             } catch (InterruptedException e) {
               res.setException(SpannerExceptionFactory.propagateInterrupt(e));
@@ -405,39 +391,37 @@ public void run() {
           }
           final CommitRequest commitRequest = requestBuilder.build();
           span.addAnnotation("Starting Commit");
-          final Span opSpan =
-              tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span).startSpan();
+          final ISpan opSpan = tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span);
           final ApiFuture<com.google.spanner.v1.CommitResponse> commitFuture =
               rpc.commitAsync(commitRequest, session.getOptions());
           session.markUsed(clock.instant());
           commitFuture.addListener(
-              tracer.withSpan(
-                  opSpan,
-                  () -> {
-                    try {
-                      com.google.spanner.v1.CommitResponse proto = commitFuture.get();
-                      if (!proto.hasCommitTimestamp()) {
-                        throw newSpannerException(
-                            ErrorCode.INTERNAL, "Missing commitTimestamp:\n" + session.getName());
-                      }
-                      span.addAnnotation("Commit Done");
-                      opSpan.end(TraceUtil.END_SPAN_OPTIONS);
-                      res.set(new CommitResponse(proto));
-                    } catch (Throwable e) {
-                      if (e instanceof ExecutionException) {
-                        e =
-                            SpannerExceptionFactory.newSpannerException(
-                                e.getCause() == null ? e : e.getCause());
-                      } else if (e instanceof InterruptedException) {
-                        e = SpannerExceptionFactory.propagateInterrupt((InterruptedException) e);
-                      } else {
-                        e = SpannerExceptionFactory.newSpannerException(e);
-                      }
-                      span.addAnnotation("Commit Failed", TraceUtil.getExceptionAnnotations(e));
-                      TraceUtil.endSpanWithFailure(opSpan, e);
-                      res.setException(onError((SpannerException) e, false));
-                    }
-                  }),
+              () -> {
+                try (IScope s = tracer.withSpan(opSpan)) {
+                  com.google.spanner.v1.CommitResponse proto = commitFuture.get();
+                  if (!proto.hasCommitTimestamp()) {
+                    throw newSpannerException(
+                        ErrorCode.INTERNAL, "Missing commitTimestamp:\n" + session.getName());
+                  }
+                  span.addAnnotation("Commit Done");
+                  opSpan.end();
+                  res.set(new CommitResponse(proto));
+                } catch (Throwable e) {
+                  if (e instanceof ExecutionException) {
+                    e =
+                        SpannerExceptionFactory.newSpannerException(
+                            e.getCause() == null ? e : e.getCause());
+                  } else if (e instanceof InterruptedException) {
+                    e = SpannerExceptionFactory.propagateInterrupt((InterruptedException) e);
+                  } else {
+                    e = SpannerExceptionFactory.newSpannerException(e);
+                  }
+                  span.addAnnotation("Commit Failed", e);
+                  opSpan.setStatus(e);
+                  opSpan.end();
+                  res.setException(onError((SpannerException) e, false));
+                }
+              },
               MoreExecutors.directExecutor());
         } catch (InterruptedException e) {
           res.setException(SpannerExceptionFactory.propagateInterrupt(e));
@@ -446,6 +430,9 @@ public void run() {
         } catch (ExecutionException e) {
           res.setException(
               SpannerExceptionFactory.newSpannerException(e.getCause() == null ? e : e.getCause()));
+        } catch (Throwable e) {
+          res.setException(
+              SpannerExceptionFactory.newSpannerException(e.getCause() == null ? e : e.getCause()));
         }
       }
     }
@@ -466,7 +453,7 @@ void rollback() {
         rollbackAsync().get();
       } catch (ExecutionException e) {
         txnLogger.log(Level.FINE, "Exception during rollback", e);
-        span.addAnnotation("Rollback Failed", TraceUtil.getExceptionAnnotations(e));
+        span.addAnnotation("Rollback Failed", e);
       } catch (InterruptedException e) {
         throw SpannerExceptionFactory.propagateInterrupt(e);
       }
@@ -962,7 +949,8 @@ public ListenableAsyncResultSet executeQueryAsync(
   private boolean blockNestedTxn = true;
   private final SessionImpl session;
   private final Options options;
-  private Span span;
+  private ISpan span;
+  private TraceWrapper tracer;
   private TransactionContextImpl txn;
   private volatile boolean isValid = true;
 
@@ -976,29 +964,31 @@ public TransactionRunner allowNestedTransaction() {
     this.session = session;
     this.options = Options.fromTransactionOptions(options);
     this.txn = session.newTransaction(this.options);
+    this.tracer = session.getTracer();
   }
 
   @Override
-  public void setSpan(Span span) {
+  public void setSpan(ISpan span) {
     this.span = span;
   }
 
   @Nullable
   @Override
   public <T> T run(TransactionCallable<T> callable) {
-    try (Scope s = tracer.withSpan(span)) {
+    try (IScope s = tracer.withSpan(span)) {
       if (blockNestedTxn) {
         SessionImpl.hasPendingTransaction.set(Boolean.TRUE);
       }
       return runInternal(callable);
     } catch (RuntimeException e) {
-      TraceUtil.setWithFailure(span, e);
+      span.setStatus(e);
       throw e;
     } finally {
       // Remove threadLocal rather than set to FALSE to avoid a possible memory leak.
       // We also do this unconditionally in case a user has modified the flag when the transaction
       // was running.
       SessionImpl.hasPendingTransaction.remove();
+      span.end();
     }
   }
 
@@ -1016,9 +1006,7 @@ private <T> T runInternal(final TransactionCallable<T> txCallable) {
           checkState(
               isValid, "TransactionRunner has been invalidated by a new operation on the session");
           attempt.incrementAndGet();
-          span.addAnnotation(
-              "Starting Transaction Attempt",
-              ImmutableMap.of("Attempt", AttributeValue.longAttributeValue(attempt.longValue())));
+          span.addAnnotation("Starting Transaction Attempt", "Attempt", attempt.longValue());
           // Only ensure that there is a transaction if we should not inline the beginTransaction
           // with the first statement.
           if (!useInlinedBegin) {
@@ -1035,8 +1023,8 @@ private <T> T runInternal(final TransactionCallable<T> txCallable) {
             if (txn.isAborted() || (e instanceof AbortedException)) {
               span.addAnnotation(
                   "Transaction Attempt Aborted in user operation. Retrying",
-                  ImmutableMap.of(
-                      "Attempt", AttributeValue.longAttributeValue(attempt.longValue())));
+                  "Attempt",
+                  attempt.longValue());
               shouldRollback = false;
               if (e instanceof AbortedException) {
                 throw e;
@@ -1052,10 +1040,8 @@ private <T> T runInternal(final TransactionCallable<T> txCallable) {
             }
             span.addAnnotation(
                 "Transaction Attempt Failed in user operation",
-                ImmutableMap.<String, AttributeValue>builder()
-                    .putAll(TraceUtil.getExceptionAnnotations(toThrow))
-                    .put("Attempt", AttributeValue.longAttributeValue(attempt.longValue()))
-                    .build());
+                ImmutableMap.of(
+                    "Attempt", attempt.longValue(), "Status", toThrow.getErrorCode().toString()));
             throw toThrow;
           } finally {
             if (shouldRollback) {
@@ -1065,23 +1051,18 @@ private <T> T runInternal(final TransactionCallable<T> txCallable) {
 
           try {
             txn.commit();
-            span.addAnnotation(
-                "Transaction Attempt Succeeded",
-                ImmutableMap.of("Attempt", AttributeValue.longAttributeValue(attempt.longValue())));
+            span.addAnnotation("Transaction Attempt Succeeded", "Attempt", attempt.longValue());
             return result;
           } catch (AbortedException e) {
             txnLogger.log(Level.FINE, "Commit aborted", e);
             span.addAnnotation(
-                "Transaction Attempt Aborted in Commit. Retrying",
-                ImmutableMap.of("Attempt", AttributeValue.longAttributeValue(attempt.longValue())));
+                "Transaction Attempt Aborted in Commit. Retrying", "Attempt", attempt.longValue());
             throw e;
           } catch (SpannerException e) {
             span.addAnnotation(
                 "Transaction Attempt Failed in Commit",
-                ImmutableMap.<String, AttributeValue>builder()
-                    .putAll(TraceUtil.getExceptionAnnotations(e))
-                    .put("Attempt", AttributeValue.longAttributeValue(attempt.longValue()))
-                    .build());
+                ImmutableMap.of(
+                    "Attempt", attempt.longValue(), "Status", e.getErrorCode().toString()));
             throw e;
           }
         };
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java
index a8618af5c79..c9aa5987663 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java
@@ -339,7 +339,7 @@ public GapicSpannerRpc(final SpannerOptions options) {
                   SpannerInterceptorProvider.create(
                           MoreObjects.firstNonNull(
                               options.getInterceptorProvider(),
-                              SpannerInterceptorProvider.createDefault()))
+                              SpannerInterceptorProvider.createDefault(options.getOpenTelemetry())))
                       // This sets the response compressor (Server -> Client).
                       .withEncoding(compressorName))
               .setHeaderProvider(headerProviderWithUserAgent)
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java
index d4725b25427..7de63dc33ba 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java
@@ -22,6 +22,7 @@
 import static com.google.cloud.spanner.spi.v1.SpannerRpcViews.SPANNER_GFE_HEADER_MISSING_COUNT;
 import static com.google.cloud.spanner.spi.v1.SpannerRpcViews.SPANNER_GFE_LATENCY;
 
+import com.google.cloud.spanner.SpannerRpcMetrics;
 import io.grpc.CallOptions;
 import io.grpc.Channel;
 import io.grpc.ClientCall;
@@ -37,6 +38,8 @@
 import io.opencensus.tags.TagValue;
 import io.opencensus.tags.Tagger;
 import io.opencensus.tags.Tags;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.regex.Matcher;
@@ -63,8 +66,23 @@ class HeaderInterceptor implements ClientInterceptor {
 
   private static final Logger LOGGER = Logger.getLogger(HeaderInterceptor.class.getName());
   private static final Level LEVEL = Level.INFO;
+  private final SpannerRpcMetrics spannerRpcMetrics;
 
-  HeaderInterceptor() {}
+  HeaderInterceptor(SpannerRpcMetrics spannerRpcMetrics) {
+    this.spannerRpcMetrics = spannerRpcMetrics;
+  }
+
+  private class SpannerProperties {
+    String projectId;
+    String instanceId;
+    String databaseId;
+
+    SpannerProperties(String projectId, String instanceId, String databaseId) {
+      this.databaseId = databaseId;
+      this.instanceId = instanceId;
+      this.projectId = projectId;
+    }
+  }
 
   @Override
   public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
@@ -72,13 +90,14 @@ public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
     return new SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
       @Override
       public void start(Listener<RespT> responseListener, Metadata headers) {
-        TagContext tagContext = getTagContext(headers, method.getFullMethodName());
+        SpannerProperties spannerProperties = createProjectPropertes(headers);
+        TagContext tagContext = getTagContext(method.getFullMethodName(), spannerProperties);
+        Attributes attributes = getMetricAttributes(method.getFullMethodName(), spannerProperties);
         super.start(
             new SimpleForwardingClientCallListener<RespT>(responseListener) {
               @Override
               public void onHeaders(Metadata metadata) {
-
-                processHeader(metadata, tagContext);
+                processHeader(metadata, tagContext, attributes);
                 super.onHeaders(metadata);
               }
             },
@@ -87,7 +106,7 @@ public void onHeaders(Metadata metadata) {
     };
   }
 
-  private void processHeader(Metadata metadata, TagContext tagContext) {
+  private void processHeader(Metadata metadata, TagContext tagContext, Attributes attributes) {
     MeasureMap measureMap = STATS_RECORDER.newMeasureMap();
     if (metadata.get(SERVER_TIMING_HEADER_KEY) != null) {
       String serverTiming = metadata.get(SERVER_TIMING_HEADER_KEY);
@@ -98,27 +117,20 @@ private void processHeader(Metadata metadata, TagContext tagContext) {
           measureMap.put(SPANNER_GFE_LATENCY, latency);
           measureMap.put(SPANNER_GFE_HEADER_MISSING_COUNT, 0L);
           measureMap.record(tagContext);
+
+          spannerRpcMetrics.recordGfeLatency(latency, attributes);
+          spannerRpcMetrics.recordGfeHeaderMissingCount(0L, attributes);
         } catch (NumberFormatException e) {
           LOGGER.log(LEVEL, "Invalid server-timing object in header", matcher.group("dur"));
         }
       }
     } else {
+      spannerRpcMetrics.recordGfeHeaderMissingCount(1L, attributes);
       measureMap.put(SPANNER_GFE_HEADER_MISSING_COUNT, 1L).record(tagContext);
     }
   }
 
-  private TagContext getTagContext(
-      String method, String projectId, String instanceId, String databaseId) {
-    return TAGGER
-        .currentBuilder()
-        .putLocal(PROJECT_ID, TagValue.create(projectId))
-        .putLocal(INSTANCE_ID, TagValue.create(instanceId))
-        .putLocal(DATABASE_ID, TagValue.create(databaseId))
-        .putLocal(METHOD, TagValue.create(method))
-        .build();
-  }
-
-  private TagContext getTagContext(Metadata headers, String method) {
+  private SpannerProperties createProjectPropertes(Metadata headers) {
     String projectId = "undefined-project";
     String instanceId = "undefined-database";
     String databaseId = "undefined-database";
@@ -137,6 +149,26 @@ private TagContext getTagContext(Metadata headers, String method) {
         LOGGER.log(LEVEL, "Error parsing google cloud resource header: " + googleResourcePrefix);
       }
     }
-    return getTagContext(method, projectId, instanceId, databaseId);
+    return new SpannerProperties(projectId, instanceId, databaseId);
+  }
+
+  private TagContext getTagContext(String method, SpannerProperties spannerProperties) {
+    return TAGGER
+        .currentBuilder()
+        .putLocal(PROJECT_ID, TagValue.create(spannerProperties.projectId))
+        .putLocal(INSTANCE_ID, TagValue.create(spannerProperties.instanceId))
+        .putLocal(DATABASE_ID, TagValue.create(spannerProperties.databaseId))
+        .putLocal(METHOD, TagValue.create(method))
+        .build();
+  }
+
+  private Attributes getMetricAttributes(String method, SpannerProperties spannerProperties) {
+    AttributesBuilder attributesBuilder = Attributes.builder();
+    attributesBuilder.put("database", spannerProperties.databaseId);
+    attributesBuilder.put("instance_id", spannerProperties.instanceId);
+    attributesBuilder.put("project_id", spannerProperties.projectId);
+    attributesBuilder.put("method", method);
+
+    return attributesBuilder.build();
   }
 }
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerInterceptorProvider.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerInterceptorProvider.java
index e45702de8df..9b1a2fd3c1f 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerInterceptorProvider.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerInterceptorProvider.java
@@ -16,9 +16,14 @@
 package com.google.cloud.spanner.spi.v1;
 
 import com.google.api.core.InternalApi;
+import com.google.api.core.ObsoleteApi;
 import com.google.api.gax.grpc.GrpcInterceptorProvider;
+import com.google.cloud.spanner.SpannerRpcMetrics;
 import com.google.common.collect.ImmutableList;
 import io.grpc.ClientInterceptor;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -29,21 +34,24 @@
  */
 @InternalApi("Exposed for testing")
 public class SpannerInterceptorProvider implements GrpcInterceptorProvider {
-
-  private static final List<ClientInterceptor> defaultInterceptors =
-      ImmutableList.of(
-          new SpannerErrorInterceptor(),
-          new LoggingInterceptor(Logger.getLogger(GapicSpannerRpc.class.getName()), Level.FINER),
-          new HeaderInterceptor());
-
   private final List<ClientInterceptor> clientInterceptors;
 
   private SpannerInterceptorProvider(List<ClientInterceptor> clientInterceptors) {
     this.clientInterceptors = clientInterceptors;
   }
 
+  @ObsoleteApi("This method always uses Global OpenTelemetry")
   public static SpannerInterceptorProvider createDefault() {
-    return new SpannerInterceptorProvider(defaultInterceptors);
+    return createDefault(GlobalOpenTelemetry.get());
+  }
+
+  public static SpannerInterceptorProvider createDefault(OpenTelemetry openTelemetry) {
+    List<ClientInterceptor> defaultInterceptorList = new ArrayList<>();
+    defaultInterceptorList.add(new SpannerErrorInterceptor());
+    defaultInterceptorList.add(
+        new LoggingInterceptor(Logger.getLogger(GapicSpannerRpc.class.getName()), Level.FINER));
+    defaultInterceptorList.add(new HeaderInterceptor(new SpannerRpcMetrics(openTelemetry)));
+    return new SpannerInterceptorProvider(ImmutableList.copyOf(defaultInterceptorList));
   }
 
   static SpannerInterceptorProvider create(GrpcInterceptorProvider provider) {
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpcViews.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpcViews.java
index b1b68e95d1a..7d6cc163b46 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpcViews.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpcViews.java
@@ -15,6 +15,8 @@
  */
 package com.google.cloud.spanner.spi.v1;
 
+import com.google.api.core.ObsoleteApi;
+import com.google.cloud.spanner.SpannerOptions;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import io.opencensus.stats.Aggregation;
@@ -91,28 +93,49 @@ public class SpannerRpcViews {
    * measures the latency between Google's network receives an RPC and reads back the first byte of
    * the response. gfe_header_missing_count is a counter of the number of RPC responses without a
    * server-timing header.
+   *
+   * @deprecated The OpenCensus project is deprecated. Use OpenTelemetry to get gfe_latency and
+   *     gfe_header_missing_count metrics.
    */
   @VisibleForTesting
+  @ObsoleteApi(
+      "The OpenCensus project is deprecated. Use OpenTelemetry to get gfe_latency and gfe_header_missing_count metrics.")
   public static void registerGfeLatencyAndHeaderMissingCountViews() {
-    viewManager.registerView(SPANNER_GFE_LATENCY_VIEW);
-    viewManager.registerView(SPANNER_GFE_HEADER_MISSING_COUNT_VIEW);
+    if (SpannerOptions.isEnabledOpenCensusMetrics()) {
+      viewManager.registerView(SPANNER_GFE_LATENCY_VIEW);
+      viewManager.registerView(SPANNER_GFE_HEADER_MISSING_COUNT_VIEW);
+    }
   }
 
   /**
    * Register GFE Latency view. gfe_latency measures the latency between Google's network receives
    * an RPC and reads back the first byte of the response.
+   *
+   * @deprecated The OpenCensus project is deprecated. Use OpenTelemetry to get gfe_latency and
+   *     gfe_header_missing_count metrics.
    */
   @VisibleForTesting
+  @ObsoleteApi(
+      "The OpenCensus project is deprecated. Use OpenTelemetry to get gfe_latency and gfe_header_missing_count metrics.")
   public static void registerGfeLatencyView() {
-    viewManager.registerView(SPANNER_GFE_LATENCY_VIEW);
+    if (SpannerOptions.isEnabledOpenCensusMetrics()) {
+      viewManager.registerView(SPANNER_GFE_LATENCY_VIEW);
+    }
   }
 
   /**
    * Register GFE Header Missing Count view. gfe_header_missing_count is a counter of the number of
    * RPC responses without a server-timing header.
+   *
+   * @deprecated The OpenCensus project is deprecated. Use OpenTelemetry to get gfe_latency and
+   *     gfe_header_missing_count metrics.
    */
   @VisibleForTesting
+  @ObsoleteApi(
+      "The OpenCensus project is deprecated. Use OpenTelemetry to get gfe_latency and gfe_header_missing_count metrics.")
   public static void registerGfeHeaderMissingCountView() {
-    viewManager.registerView(SPANNER_GFE_HEADER_MISSING_COUNT_VIEW);
+    if (SpannerOptions.isEnabledOpenCensusMetrics()) {
+      viewManager.registerView(SPANNER_GFE_HEADER_MISSING_COUNT_VIEW);
+    }
   }
 }
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java
index 84f7fb0db9b..08d22dd2d67 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java
@@ -22,7 +22,8 @@
 
 import com.google.api.core.ApiFutures;
 import com.google.cloud.Timestamp;
-import io.opencensus.trace.Span;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Scope;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -36,8 +37,11 @@ public class AsyncTransactionManagerImplTest {
 
   @Test
   public void testCommitReturnsCommitStats() {
+    Span oTspan = mock(Span.class);
+    ISpan span = new OpenTelemetrySpan(oTspan);
+    when(oTspan.makeCurrent()).thenReturn(mock(Scope.class));
     try (AsyncTransactionManagerImpl manager =
-        new AsyncTransactionManagerImpl(session, mock(Span.class), Options.commitStats())) {
+        new AsyncTransactionManagerImpl(session, span, Options.commitStats())) {
       when(session.newTransaction(Options.fromTransactionOptions(Options.commitStats())))
           .thenReturn(transaction);
       when(transaction.ensureTxnAsync()).thenReturn(ApiFutures.immediateFuture(null));
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java
index 18ae8a07b35..b7c4834044a 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java
@@ -35,9 +35,11 @@
 import com.google.protobuf.util.Timestamps;
 import com.google.spanner.v1.Session;
 import com.google.spanner.v1.Transaction;
+import io.opentelemetry.api.OpenTelemetry;
 import java.util.Collections;
 import java.util.Map;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -62,6 +64,12 @@ public final class BatchClientImplTest {
 
   private BatchClient client;
 
+  @BeforeClass
+  public static void setupOpenTelemetry() {
+    SpannerOptions.resetActiveTracingFramework();
+    SpannerOptions.enableOpenTelemetryTraces();
+  }
+
   @SuppressWarnings("unchecked")
   @Before
   public void setUp() {
@@ -74,6 +82,7 @@ public void setUp() {
     when(spannerOptions.getClock()).thenReturn(NanoClock.getDefaultClock());
     when(spannerOptions.getSpannerRpcV1()).thenReturn(gapicRpc);
     when(spannerOptions.getSessionLabels()).thenReturn(Collections.emptyMap());
+    when(spannerOptions.getOpenTelemetry()).thenReturn(OpenTelemetry.noop());
     GrpcTransportOptions transportOptions = mock(GrpcTransportOptions.class);
     when(transportOptions.getExecutorFactory()).thenReturn(mock(ExecutorFactory.class));
     when(spannerOptions.getTransportOptions()).thenReturn(transportOptions);
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java
index e527660cfa8..ccbf3c0b2b9 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java
@@ -102,6 +102,8 @@
 import io.grpc.StatusRuntimeException;
 import io.grpc.inprocess.InProcessServerBuilder;
 import io.grpc.protobuf.lite.ProtoLiteUtils;
+import io.opencensus.trace.Tracing;
+import io.opentelemetry.api.OpenTelemetry;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
@@ -3375,7 +3377,10 @@ public void testReadWriteTransaction_usesOptions() {
     when(pool.getSession()).thenReturn(session);
     TransactionOption option = mock(TransactionOption.class);
 
-    DatabaseClientImpl client = new DatabaseClientImpl(pool);
+    TraceWrapper traceWrapper =
+        new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""));
+
+    DatabaseClientImpl client = new DatabaseClientImpl(pool, traceWrapper);
     client.readWriteTransaction(option);
 
     verify(session).readWriteTransaction(option);
@@ -3388,7 +3393,7 @@ public void testTransactionManager_usesOptions() {
     when(pool.getSession()).thenReturn(session);
     TransactionOption option = mock(TransactionOption.class);
 
-    DatabaseClientImpl client = new DatabaseClientImpl(pool);
+    DatabaseClientImpl client = new DatabaseClientImpl(pool, mock(TraceWrapper.class));
     try (TransactionManager ignore = client.transactionManager(option)) {
       verify(session).transactionManager(option);
     }
@@ -3401,7 +3406,7 @@ public void testRunAsync_usesOptions() {
     when(pool.getSession()).thenReturn(session);
     TransactionOption option = mock(TransactionOption.class);
 
-    DatabaseClientImpl client = new DatabaseClientImpl(pool);
+    DatabaseClientImpl client = new DatabaseClientImpl(pool, mock(TraceWrapper.class));
     client.runAsync(option);
 
     verify(session).runAsync(option);
@@ -3414,7 +3419,7 @@ public void testTransactionManagerAsync_usesOptions() {
     when(pool.getSession()).thenReturn(session);
     TransactionOption option = mock(TransactionOption.class);
 
-    DatabaseClientImpl client = new DatabaseClientImpl(pool);
+    DatabaseClientImpl client = new DatabaseClientImpl(pool, mock(TraceWrapper.class));
     try (AsyncTransactionManager ignore = client.transactionManagerAsync(option)) {
       verify(session).transactionManagerAsync(option);
     }
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/FailOnOverkillTraceComponentImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/FailOnOverkillTraceComponentImpl.java
index 4877c60837f..4d9b4ddd805 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/FailOnOverkillTraceComponentImpl.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/FailOnOverkillTraceComponentImpl.java
@@ -29,6 +29,7 @@
 import io.opencensus.trace.SpanBuilder;
 import io.opencensus.trace.SpanContext;
 import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
 import io.opencensus.trace.TraceComponent;
 import io.opencensus.trace.TraceId;
 import io.opencensus.trace.TraceOptions;
@@ -43,6 +44,7 @@
 import io.opencensus.trace.propagation.BinaryFormat;
 import io.opencensus.trace.propagation.PropagationComponent;
 import io.opencensus.trace.propagation.TextFormat;
+import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -62,6 +64,8 @@ public class FailOnOverkillTraceComponentImpl extends TraceComponent {
   private final TraceConfig traceConfig = new TestTraceConfig();
   private static final Map<String, Boolean> spans = new LinkedHashMap<>();
 
+  private static final List<String> annotations = new ArrayList<>();
+
   public static class TestSpan extends Span {
     @GuardedBy("this")
     private volatile boolean ended = false;
@@ -75,14 +79,27 @@ private TestSpan(String spanName, SpanContext context, EnumSet<Options> options)
     }
 
     @Override
-    public void addAnnotation(String description, Map<String, AttributeValue> attributes) {}
+    public void addAnnotation(String description, Map<String, AttributeValue> attributes) {
+      annotations.add(description);
+    }
+
+    @Override
+    public void addAnnotation(Annotation annotation) {
+      annotations.add(annotation.getDescription());
+    }
+
+    @Override
+    public void putAttributes(Map<String, AttributeValue> attributes) {}
 
     @Override
-    public void addAnnotation(Annotation annotation) {}
+    public void addAttributes(Map<String, AttributeValue> attributes) {}
 
     @Override
     public void addLink(Link link) {}
 
+    @Override
+    public void setStatus(Status status) {}
+
     @Override
     public void end(EndSpanOptions options) {
       synchronized (this) {
@@ -210,10 +227,18 @@ Map<String, Boolean> getSpans() {
     return spans;
   }
 
+  List<String> getAnnotations() {
+    return annotations;
+  }
+
   void clearSpans() {
     spans.clear();
   }
 
+  void clearAnnotations() {
+    annotations.clear();
+  }
+
   @Override
   public PropagationComponent getPropagationComponent() {
     return propagationComponent;
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java
index 36b9bee0af7..be9f6841f20 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java
@@ -20,6 +20,8 @@
 
 import com.google.cloud.grpc.GrpcTransportOptions.ExecutorFactory;
 import com.google.cloud.spanner.SessionPool.PooledSessionFuture;
+import io.opencensus.trace.Tracing;
+import io.opentelemetry.api.OpenTelemetry;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -93,7 +95,9 @@ public ScheduledExecutorService get() {
                 return new ScheduledThreadPoolExecutor(2);
               }
             },
-            ((SpannerImpl) env.getTestHelper().getClient()).getSessionClient(db.getId()));
+            ((SpannerImpl) env.getTestHelper().getClient()).getSessionClient(db.getId()),
+            new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer("")),
+            OpenTelemetry.noop());
   }
 
   @Test
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java
index 0fa5ff27d82..62a25f9dc4d 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java
@@ -46,7 +46,7 @@ private static class SpannerWithClosedSessionsImpl extends SpannerImpl {
 
     @Override
     DatabaseClientImpl createDatabaseClient(String clientId, SessionPool pool) {
-      return new DatabaseClientWithClosedSessionImpl(clientId, pool);
+      return new DatabaseClientWithClosedSessionImpl(clientId, pool, tracer);
     }
   }
 
@@ -58,8 +58,8 @@ public static class DatabaseClientWithClosedSessionImpl extends DatabaseClientIm
     private boolean invalidateNextSession = false;
     private boolean allowReplacing = true;
 
-    DatabaseClientWithClosedSessionImpl(String clientId, SessionPool pool) {
-      super(clientId, pool);
+    DatabaseClientWithClosedSessionImpl(String clientId, SessionPool pool, TraceWrapper tracer) {
+      super(clientId, pool, tracer);
     }
 
     /** Invalidate the next session that is checked out from the pool. */
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetrySpanTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetrySpanTest.java
new file mode 100644
index 00000000000..956607e0231
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetrySpanTest.java
@@ -0,0 +1,563 @@
+/*
+ * Copyright 2023 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.google.api.gax.grpc.testing.LocalChannelProvider;
+import com.google.cloud.NoCredentials;
+import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ListValue;
+import com.google.spanner.v1.ResultSetMetadata;
+import com.google.spanner.v1.StructType;
+import com.google.spanner.v1.StructType.Field;
+import com.google.spanner.v1.TypeCode;
+import io.grpc.Server;
+import io.grpc.Status;
+import io.grpc.inprocess.InProcessServerBuilder;
+import io.opencensus.trace.Tracing;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
+import io.opentelemetry.context.propagation.ContextPropagators;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.data.EventData;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@Category(TracerTest.class)
+@RunWith(JUnit4.class)
+public class OpenTelemetrySpanTest {
+
+  private static final String TEST_PROJECT = "my-project";
+  private static final String TEST_INSTANCE = "my-instance";
+  private static final String TEST_DATABASE = "my-database";
+  private static LocalChannelProvider channelProvider;
+  private static MockSpannerServiceImpl mockSpanner;
+  private Spanner spanner;
+  private DatabaseClient client;
+  private static Server server;
+  private static InMemorySpanExporter spanExporter;
+
+  private static FailOnOverkillTraceComponentImpl failOnOverkillTraceComponent =
+      new FailOnOverkillTraceComponentImpl();
+
+  private static final Statement SELECT1 = Statement.of("SELECT 1 AS COL1");
+
+  private static final ResultSetMetadata SELECT1_METADATA =
+      ResultSetMetadata.newBuilder()
+          .setRowType(
+              StructType.newBuilder()
+                  .addFields(
+                      Field.newBuilder()
+                          .setName("COL1")
+                          .setType(
+                              com.google.spanner.v1.Type.newBuilder()
+                                  .setCode(TypeCode.INT64)
+                                  .build())
+                          .build())
+                  .build())
+          .build();
+  private static final com.google.spanner.v1.ResultSet SELECT1_RESULTSET =
+      com.google.spanner.v1.ResultSet.newBuilder()
+          .addRows(
+              ListValue.newBuilder()
+                  .addValues(com.google.protobuf.Value.newBuilder().setStringValue("1").build())
+                  .build())
+          .setMetadata(SELECT1_METADATA)
+          .build();
+  private static final Statement UPDATE_STATEMENT =
+      Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2");
+  private static final long UPDATE_COUNT = 1L;
+  private static final Statement INVALID_UPDATE_STATEMENT =
+      Statement.of("UPDATE NON_EXISTENT_TABLE SET BAR=1 WHERE BAZ=2");
+
+  private List<String> expectedBatchCreateSessionsRequestEvents =
+      ImmutableList.of("Requesting 25 sessions", "Request for 25 sessions returned 25 sessions");
+
+  private int expectedBatchCreateSessionsRequestEventsCount = 2;
+
+  private List<String> expectedBatchCreateSessionsEvents = ImmutableList.of("Creating 25 sessions");
+
+  private int expectedBatchCreateSessionsEventsCount = 1;
+
+  private List<String> expectedExecuteStreamingQueryEvents =
+      ImmutableList.of("Starting/Resuming stream");
+
+  private int expectedExecuteStreamingQueryEventsCount = 1;
+
+  private List<String> expectedReadOnlyTransactionSingleUseEvents =
+      ImmutableList.of(
+          "Acquiring session",
+          "No session available",
+          "Creating sessions",
+          "Waiting for a session to come available",
+          "Using Session");
+
+  private int expectedReadOnlyTransactionSingleUseEventsCount = 5;
+
+  private List<String> expectedReadOnlyTransactionMultiUseEvents =
+      ImmutableList.of(
+          "Acquiring session",
+          "No session available",
+          "Creating sessions",
+          "Waiting for a session to come available",
+          "Using Session",
+          "Creating Transaction",
+          "Transaction Creation Done");
+
+  private int expectedReadOnlyTransactionMultiUseEventsCount = 7;
+
+  private List<String> expectedReadWriteTransactionErrorEvents =
+      ImmutableList.of(
+          "Acquiring session",
+          "No session available",
+          "Creating sessions",
+          "Waiting for a session to come available",
+          "Using Session",
+          "Starting Transaction Attempt",
+          "Transaction Attempt Failed in user operation",
+          "exception");
+
+  private int expectedReadWriteTransactionErrorEventsCount = 8;
+  private List<String> expectedReadWriteTransactionEvents =
+      ImmutableList.of(
+          "Acquiring session",
+          "No session available",
+          "Creating sessions",
+          "Waiting for a session to come available",
+          "Using Session",
+          "Starting Transaction Attempt",
+          "Starting Commit",
+          "Commit Done",
+          "Transaction Attempt Succeeded");
+
+  private int expectedReadWriteTransactionCount = 9;
+  private List<String> expectedReadWriteTransactionErrorWithBeginTransactionEvents =
+      ImmutableList.of(
+          "Acquiring session",
+          "No session available",
+          "Creating sessions",
+          "Waiting for a session to come available",
+          "Using Session",
+          "Starting Transaction Attempt",
+          "Transaction Attempt Aborted in user operation. Retrying",
+          "Creating Transaction",
+          "Transaction Creation Done",
+          "Starting Commit",
+          "Commit Done",
+          "Transaction Attempt Succeeded");
+
+  private int expectedReadWriteTransactionErrorWithBeginTransactionEventsCount = 13;
+  private List<String> expectedReadOnlyTransactionSpans =
+      ImmutableList.of(
+          "CloudSpannerOperation.BatchCreateSessionsRequest",
+          "CloudSpannerOperation.ExecuteStreamingQuery",
+          "CloudSpannerOperation.BatchCreateSessions",
+          "CloudSpanner.ReadOnlyTransaction",
+          "SessionPool.WaitForSession");
+
+  private List<String> expectedReadWriteTransactionWithCommitSpans =
+      ImmutableList.of(
+          "CloudSpannerOperation.BatchCreateSessionsRequest",
+          "CloudSpannerOperation.Commit",
+          "CloudSpannerOperation.BatchCreateSessions",
+          "CloudSpanner.ReadWriteTransaction",
+          "SessionPool.WaitForSession");
+
+  private List<String> expectedReadWriteTransactionSpans =
+      ImmutableList.of(
+          "CloudSpannerOperation.BatchCreateSessionsRequest",
+          "CloudSpannerOperation.BatchCreateSessions",
+          "CloudSpanner.ReadWriteTransaction",
+          "SessionPool.WaitForSession");
+
+  private List<String> expectedReadWriteTransactionWithCommitAndBeginTransactionSpans =
+      ImmutableList.of(
+          "CloudSpannerOperation.BeginTransaction",
+          "CloudSpannerOperation.BatchCreateSessionsRequest",
+          "CloudSpannerOperation.Commit",
+          "CloudSpannerOperation.BatchCreateSessions",
+          "CloudSpanner.ReadWriteTransaction",
+          "SessionPool.WaitForSession");
+
+  @BeforeClass
+  public static void setupOpenTelemetry() {
+    SpannerOptions.resetActiveTracingFramework();
+    SpannerOptions.enableOpenTelemetryTraces();
+  }
+
+  @BeforeClass
+  public static void startStaticServer() throws Exception {
+    // Incorporating OpenCensus tracer to ensure that OpenTraces traces are utilized if enabled,
+    // regardless of the presence of OpenCensus tracer.
+    java.lang.reflect.Field field = Tracing.class.getDeclaredField("traceComponent");
+    field.setAccessible(true);
+    java.lang.reflect.Field modifiersField = null;
+    try {
+      modifiersField = java.lang.reflect.Field.class.getDeclaredField("modifiers");
+    } catch (NoSuchFieldException e) {
+      // Halt the test and ignore it.
+      Assume.assumeTrue(
+          "Skipping test as reflection is not allowed on reflection class in this JDK build",
+          false);
+    }
+    modifiersField.setAccessible(true);
+    // Remove the final modifier from the 'traceComponent' field.
+    modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
+    field.set(null, failOnOverkillTraceComponent);
+
+    mockSpanner = new MockSpannerServiceImpl();
+    mockSpanner.setAbortProbability(0.0D); // We don't want any unpredictable aborted transactions.
+    mockSpanner.putStatementResult(StatementResult.query(SELECT1, SELECT1_RESULTSET));
+    mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT));
+    mockSpanner.putStatementResult(
+        StatementResult.exception(
+            INVALID_UPDATE_STATEMENT,
+            Status.INVALID_ARGUMENT.withDescription("invalid statement").asRuntimeException()));
+    String uniqueName = InProcessServerBuilder.generateName();
+    server = InProcessServerBuilder.forName(uniqueName).addService(mockSpanner).build().start();
+
+    channelProvider = LocalChannelProvider.create(uniqueName);
+    failOnOverkillTraceComponent.clearSpans();
+    failOnOverkillTraceComponent.clearAnnotations();
+  }
+
+  @AfterClass
+  public static void stopServer() throws InterruptedException {
+    if (server != null) {
+      server.shutdown();
+      server.awaitTermination();
+    }
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    spanExporter = InMemorySpanExporter.create();
+
+    SdkTracerProvider tracerProvider =
+        SdkTracerProvider.builder()
+            .addSpanProcessor(SimpleSpanProcessor.create(spanExporter))
+            .build();
+
+    OpenTelemetry openTelemetry =
+        OpenTelemetrySdk.builder()
+            .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
+            .setTracerProvider(tracerProvider)
+            .build();
+
+    SpannerOptions.Builder builder =
+        SpannerOptions.newBuilder()
+            .setProjectId(TEST_PROJECT)
+            .setChannelProvider(channelProvider)
+            .setOpenTelemetry(openTelemetry)
+            .setCredentials(NoCredentials.getInstance())
+            .setSessionPoolOption(SessionPoolOptions.newBuilder().setMinSessions(0).build());
+
+    spanner = builder.build().getService();
+
+    client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
+  }
+
+  @After
+  public void tearDown() {
+    spanner.close();
+    mockSpanner.reset();
+    mockSpanner.removeAllExecutionTimes();
+    spanExporter.reset();
+  }
+
+  @Test
+  public void singleUse() {
+    try (ResultSet rs = client.singleUse().executeQuery(SELECT1)) {
+      while (rs.next()) {
+        // Just consume the result set.
+      }
+    }
+
+    // OpenCensus spans should be 0 as OpenTelemetry is enabled.
+    assertEquals(failOnOverkillTraceComponent.getSpans().size(), 0);
+
+    List<String> actualSpanItems = new ArrayList<>();
+    spanExporter
+        .getFinishedSpanItems()
+        .forEach(
+            spanItem -> {
+              actualSpanItems.add(spanItem.getName());
+              switch (spanItem.getName()) {
+                case "CloudSpannerOperation.BatchCreateSessionsRequest":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedBatchCreateSessionsRequestEvents,
+                      expectedBatchCreateSessionsRequestEventsCount);
+                  break;
+                case "CloudSpannerOperation.BatchCreateSessions":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedBatchCreateSessionsEvents,
+                      expectedBatchCreateSessionsEventsCount);
+                  break;
+                case "CloudSpannerOperation.ExecuteStreamingQuery":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedExecuteStreamingQueryEvents,
+                      expectedExecuteStreamingQueryEventsCount);
+                  break;
+                case "CloudSpanner.ReadOnlyTransaction":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedReadOnlyTransactionSingleUseEvents,
+                      expectedReadOnlyTransactionSingleUseEventsCount);
+                  break;
+                case "SessionPool.WaitForSession":
+                  assertEquals(0, spanItem.getEvents().size());
+                  break;
+                default:
+                  assert false;
+              }
+            });
+
+    verifySpans(actualSpanItems, expectedReadOnlyTransactionSpans);
+  }
+
+  @Test
+  public void multiUse() {
+    try (ReadOnlyTransaction tx = client.readOnlyTransaction()) {
+      try (ResultSet rs = tx.executeQuery(SELECT1)) {
+        while (rs.next()) {
+          // Just consume the result set.
+        }
+      }
+    }
+
+    List<String> actualSpanItems = new ArrayList<>();
+    spanExporter
+        .getFinishedSpanItems()
+        .forEach(
+            spanItem -> {
+              actualSpanItems.add(spanItem.getName());
+              switch (spanItem.getName()) {
+                case "CloudSpannerOperation.BatchCreateSessionsRequest":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedBatchCreateSessionsRequestEvents,
+                      expectedBatchCreateSessionsRequestEventsCount);
+                  break;
+                case "CloudSpannerOperation.BatchCreateSessions":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedBatchCreateSessionsEvents,
+                      expectedBatchCreateSessionsEventsCount);
+                  break;
+                case "CloudSpannerOperation.ExecuteStreamingQuery":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedExecuteStreamingQueryEvents,
+                      expectedExecuteStreamingQueryEventsCount);
+                  break;
+                case "SessionPool.WaitForSession":
+                  assertEquals(0, spanItem.getEvents().size());
+                  break;
+                case "CloudSpanner.ReadOnlyTransaction":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedReadOnlyTransactionMultiUseEvents,
+                      expectedReadOnlyTransactionMultiUseEventsCount);
+                  break;
+                default:
+                  assert false;
+              }
+            });
+
+    verifySpans(actualSpanItems, expectedReadOnlyTransactionSpans);
+  }
+
+  @Test
+  public void transactionRunner() {
+    TransactionRunner runner = client.readWriteTransaction();
+    runner.run(transaction -> transaction.executeUpdate(UPDATE_STATEMENT));
+
+    List<String> actualSpanItems = new ArrayList<>();
+    spanExporter
+        .getFinishedSpanItems()
+        .forEach(
+            spanItem -> {
+              actualSpanItems.add(spanItem.getName());
+              switch (spanItem.getName()) {
+                case "CloudSpannerOperation.BatchCreateSessionsRequest":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedBatchCreateSessionsRequestEvents,
+                      expectedBatchCreateSessionsRequestEventsCount);
+                  break;
+                case "CloudSpannerOperation.BatchCreateSessions":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedBatchCreateSessionsEvents,
+                      expectedBatchCreateSessionsEventsCount);
+                  break;
+                case "SessionPool.WaitForSession":
+                case "CloudSpannerOperation.Commit":
+                  assertEquals(0, spanItem.getEvents().size());
+                  break;
+                case "CloudSpanner.ReadWriteTransaction":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedReadWriteTransactionEvents,
+                      expectedReadWriteTransactionCount);
+                  break;
+                default:
+                  assert false;
+              }
+            });
+
+    verifySpans(actualSpanItems, expectedReadWriteTransactionWithCommitSpans);
+  }
+
+  @Test
+  public void transactionRunnerWithError() {
+    TransactionRunner runner = client.readWriteTransaction();
+    SpannerException e =
+        assertThrows(
+            SpannerException.class,
+            () -> runner.run(transaction -> transaction.executeUpdate(INVALID_UPDATE_STATEMENT)));
+    assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
+
+    List<String> actualSpanItems = new ArrayList<>();
+    spanExporter
+        .getFinishedSpanItems()
+        .forEach(
+            spanItem -> {
+              actualSpanItems.add(spanItem.getName());
+              switch (spanItem.getName()) {
+                case "CloudSpannerOperation.BatchCreateSessionsRequest":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedBatchCreateSessionsRequestEvents,
+                      expectedBatchCreateSessionsRequestEventsCount);
+                  break;
+                case "CloudSpannerOperation.BatchCreateSessions":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedBatchCreateSessionsEvents,
+                      expectedBatchCreateSessionsEventsCount);
+                  break;
+                case "SessionPool.WaitForSession":
+                  assertEquals(0, spanItem.getEvents().size());
+                  break;
+                case "CloudSpanner.ReadWriteTransaction":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedReadWriteTransactionErrorEvents,
+                      expectedReadWriteTransactionErrorEventsCount);
+                  break;
+                default:
+                  assert false;
+              }
+            });
+
+    verifySpans(actualSpanItems, expectedReadWriteTransactionSpans);
+  }
+
+  @Test
+  public void transactionRunnerWithFailedAndBeginTransaction() {
+    Long updateCount =
+        client
+            .readWriteTransaction()
+            .run(
+                transaction -> {
+                  // This update statement carries the BeginTransaction, but fails. This will
+                  // cause the entire transaction to be retried with an explicit
+                  // BeginTransaction RPC to ensure all statements in the transaction are
+                  // actually executed against the same transaction.
+                  SpannerException e =
+                      assertThrows(
+                          SpannerException.class,
+                          () -> transaction.executeUpdate(INVALID_UPDATE_STATEMENT));
+                  assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
+                  return transaction.executeUpdate(UPDATE_STATEMENT);
+                });
+
+    List<String> actualSpanItems = new ArrayList<>();
+    spanExporter
+        .getFinishedSpanItems()
+        .forEach(
+            spanItem -> {
+              actualSpanItems.add(spanItem.getName());
+              switch (spanItem.getName()) {
+                case "CloudSpannerOperation.BatchCreateSessionsRequest":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedBatchCreateSessionsRequestEvents,
+                      expectedBatchCreateSessionsRequestEventsCount);
+                  break;
+                case "CloudSpannerOperation.BatchCreateSessions":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedBatchCreateSessionsEvents,
+                      expectedBatchCreateSessionsEventsCount);
+                  break;
+                case "SessionPool.WaitForSession":
+                case "CloudSpannerOperation.Commit":
+                case "CloudSpannerOperation.BeginTransaction":
+                  assertEquals(0, spanItem.getEvents().size());
+                  break;
+                case "CloudSpanner.ReadWriteTransaction":
+                  verifyRequestEvents(
+                      spanItem,
+                      expectedReadWriteTransactionErrorWithBeginTransactionEvents,
+                      expectedReadWriteTransactionErrorWithBeginTransactionEventsCount);
+                  break;
+                default:
+                  assert false;
+              }
+            });
+
+    verifySpans(actualSpanItems, expectedReadWriteTransactionWithCommitAndBeginTransactionSpans);
+  }
+
+  private void verifyRequestEvents(SpanData spanItem, List<String> expectedEvents, int eventCount) {
+    List<String> eventNames =
+        spanItem.getEvents().stream().map(EventData::getName).collect(Collectors.toList());
+    assertEquals(eventCount, spanItem.getEvents().size());
+    assertEquals(
+        eventNames.stream().distinct().sorted().collect(Collectors.toList()),
+        expectedEvents.stream().sorted().collect(Collectors.toList()));
+  }
+
+  private static void verifySpans(List<String> actualSpanItems, List<String> expectedSpansItems) {
+    assertEquals(
+        actualSpanItems.stream().distinct().sorted().collect(Collectors.toList()),
+        expectedSpansItems.stream().sorted().collect(Collectors.toList()));
+  }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java
index 06c1725d76a..217e818d42c 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertThrows;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.google.api.client.util.BackOff;
 import com.google.cloud.spanner.AbstractResultSet.ResumableStreamIterator;
@@ -36,8 +37,10 @@
 import io.grpc.Status;
 import io.grpc.StatusRuntimeException;
 import io.grpc.protobuf.ProtoUtils;
-import io.opencensus.trace.EndSpanOptions;
 import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracing;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.context.Scope;
 import java.io.IOException;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
@@ -132,15 +135,19 @@ public boolean isWithBeginTransaction() {
 
   @Before
   public void setUp() {
+    SpannerOptions.resetActiveTracingFramework();
+    SpannerOptions.enableOpenTelemetryTraces();
     initWithLimit(Integer.MAX_VALUE);
   }
 
   private void initWithLimit(int maxBufferSize) {
+
     resumableStreamIterator =
         new AbstractResultSet.ResumableStreamIterator(
             maxBufferSize,
             "",
-            null,
+            new OpenTelemetrySpan(mock(io.opentelemetry.api.trace.Span.class)),
+            new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer("")),
             SpannerStubSettings.newBuilder().executeStreamingSqlSettings().getRetrySettings(),
             SpannerStubSettings.newBuilder().executeStreamingSqlSettings().getRetryableCodes()) {
           @Override
@@ -163,12 +170,39 @@ public void simple() {
   }
 
   @Test
-  public void closedSpan() {
+  public void closedOTSpan() {
+    SpannerOptions.resetActiveTracingFramework();
+    SpannerOptions.enableOpenTelemetryTraces();
     Assume.assumeTrue(
         "This test is only supported on JDK11 and lower",
         JavaVersionUtil.getJavaMajorVersion() < 12);
 
-    Span span = mock(Span.class);
+    io.opentelemetry.api.trace.Span oTspan = mock(io.opentelemetry.api.trace.Span.class);
+    ISpan span = new OpenTelemetrySpan(oTspan);
+    when(oTspan.makeCurrent()).thenReturn(mock(Scope.class));
+    setInternalState(ResumableStreamIterator.class, this.resumableStreamIterator, "span", span);
+
+    ResultSetStream s1 = Mockito.mock(ResultSetStream.class);
+    Mockito.when(starter.startStream(null)).thenReturn(new ResultSetIterator(s1));
+    Mockito.when(s1.next())
+        .thenReturn(resultSet(ByteString.copyFromUtf8("r1"), "a"))
+        .thenReturn(resultSet(ByteString.copyFromUtf8("r2"), "b"))
+        .thenReturn(null);
+    assertThat(consume(resumableStreamIterator)).containsExactly("a", "b").inOrder();
+
+    resumableStreamIterator.close("closed");
+    verify(oTspan).end();
+  }
+
+  @Test
+  public void closedOCSpan() {
+    SpannerOptions.resetActiveTracingFramework();
+    SpannerOptions.enableOpenCensusTraces();
+    Assume.assumeTrue(
+        "This test is only supported on JDK11 and lower",
+        JavaVersionUtil.getJavaMajorVersion() < 12);
+    Span mockSpan = mock(Span.class);
+    ISpan span = new OpenCensusSpan(mockSpan);
     setInternalState(ResumableStreamIterator.class, this.resumableStreamIterator, "span", span);
 
     ResultSetStream s1 = Mockito.mock(ResultSetStream.class);
@@ -180,7 +214,7 @@ public void closedSpan() {
     assertThat(consume(resumableStreamIterator)).containsExactly("a", "b").inOrder();
 
     resumableStreamIterator.close("closed");
-    verify(span).end(EndSpanOptions.builder().setSampleToLocalSpanStore(true).build());
+    verify(mockSpan).end(OpenCensusSpan.END_SPAN_OPTIONS);
   }
 
   @Test
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java
index 8af1323238a..c8d9bc39339 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java
@@ -17,6 +17,8 @@
 package com.google.cloud.spanner;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -27,6 +29,8 @@
 import com.google.cloud.spanner.SessionClient.SessionConsumer;
 import com.google.cloud.spanner.spi.v1.SpannerRpc;
 import com.google.cloud.spanner.spi.v1.SpannerRpc.Option;
+import io.opencensus.trace.Tracing;
+import io.opentelemetry.api.OpenTelemetry;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -83,6 +87,9 @@ public static Collection<Object[]> data() {
   @Mock private SpannerImpl spanner;
   @Mock private SpannerRpc rpc;
   @Mock private SpannerOptions spannerOptions;
+  private final TraceWrapper tracer =
+      new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""));
+  @Mock private ISpan span;
   @Captor ArgumentCaptor<Map<SpannerRpc.Option, Object>> options;
 
   @Before
@@ -109,6 +116,10 @@ public ScheduledExecutorService get() {
     when(spannerOptions.getRetrySettings()).thenReturn(RetrySettings.newBuilder().build());
     when(spannerOptions.getClock()).thenReturn(NanoClock.getDefaultClock());
     when(spanner.getOptions()).thenReturn(spannerOptions);
+    when(spanner.getTracer()).thenReturn(tracer);
+    doNothing().when(span).setStatus(any(Throwable.class));
+    doNothing().when(span).end();
+    doNothing().when(span).addAnnotation("Starting Commit");
     when(spanner.getRpc()).thenReturn(rpc);
   }
 
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java
index a920fdb67dc..87edb64134c 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -48,7 +49,9 @@
 import com.google.spanner.v1.RollbackRequest;
 import com.google.spanner.v1.Session;
 import com.google.spanner.v1.Transaction;
-import io.opencensus.trace.Span;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Scope;
 import java.text.ParseException;
 import java.util.Calendar;
 import java.util.Collections;
@@ -58,6 +61,7 @@
 import java.util.concurrent.TimeUnit;
 import javax.annotation.Nullable;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -76,6 +80,12 @@ public class SessionImplTest {
   @Captor private ArgumentCaptor<Map<SpannerRpc.Option, Object>> optionsCaptor;
   private Map<SpannerRpc.Option, Object> options;
 
+  @BeforeClass
+  public static void setupOpenTelemetry() {
+    SpannerOptions.resetActiveTracingFramework();
+    SpannerOptions.enableOpenTelemetryTraces();
+  }
+
   @SuppressWarnings("unchecked")
   @Before
   public void setUp() {
@@ -90,6 +100,7 @@ public void setUp() {
     when(transportOptions.getExecutorFactory()).thenReturn(mock(ExecutorFactory.class));
     when(spannerOptions.getTransportOptions()).thenReturn(transportOptions);
     when(spannerOptions.getSessionPoolOptions()).thenReturn(mock(SessionPoolOptions.class));
+    when(spannerOptions.getOpenTelemetry()).thenReturn(OpenTelemetry.noop());
     @SuppressWarnings("resource")
     SpannerImpl spanner = new SpannerImpl(rpc, spannerOptions);
     String dbName = "projects/p1/instances/i1/databases/d1";
@@ -127,7 +138,10 @@ public void setUp() {
         .thenReturn(
             SpannerStubSettings.newBuilder().executeStreamingSqlSettings().getRetryableCodes());
     session = spanner.getSessionClient(db).createSession();
-    ((SessionImpl) session).setCurrentSpan(mock(Span.class));
+    Span oTspan = mock(Span.class);
+    ISpan span = new OpenTelemetrySpan(oTspan);
+    when(oTspan.makeCurrent()).thenReturn(mock(Scope.class));
+    ((SessionImpl) session).setCurrentSpan(span);
     // We expect the same options, "options", on all calls on "session".
     options = optionsCaptor.getValue();
   }
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java
index 010e13c4390..127ffd3bff6 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java
@@ -28,6 +28,8 @@
 import com.google.cloud.spanner.SessionPool.PooledSessionFuture;
 import com.google.cloud.spanner.SessionPool.Position;
 import com.google.cloud.spanner.SessionPool.SessionConsumerImpl;
+import io.opencensus.trace.Tracing;
+import io.opentelemetry.api.OpenTelemetry;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -117,7 +119,13 @@ private SessionPool createPool() throws Exception {
     // otherwise impossible to know which session exactly is getting pinged at what point in time.
     SessionPool pool =
         SessionPool.createPool(
-            options, new TestExecutorFactory(), client.getSessionClient(db), clock, Position.FIRST);
+            options,
+            new TestExecutorFactory(),
+            client.getSessionClient(db),
+            clock,
+            Position.FIRST,
+            new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer("")),
+            OpenTelemetry.noop());
     pool.idleSessionRemovedListener =
         input -> {
           idledSessions.add(input);
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java
index 535423bb9e4..bb263f04e5f 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java
@@ -34,6 +34,8 @@
 import com.google.common.util.concurrent.Uninterruptibles;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.Empty;
+import io.opencensus.trace.Tracing;
+import io.opentelemetry.api.OpenTelemetry;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -248,7 +250,9 @@ public void stressTest() throws Exception {
             new TestExecutorFactory(),
             mockSpanner.getSessionClient(db),
             clock,
-            Position.RANDOM);
+            Position.RANDOM,
+            new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer("")),
+            OpenTelemetry.noop());
     pool.idleSessionRemovedListener =
         pooled -> {
           String name = pooled.getName();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java
index 4db74514805..e1c295c14de 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java
@@ -16,9 +16,18 @@
 
 package com.google.cloud.spanner;
 
+import static com.google.cloud.spanner.MetricRegistryConstants.GET_SESSION_TIMEOUTS;
+import static com.google.cloud.spanner.MetricRegistryConstants.MAX_ALLOWED_SESSIONS;
+import static com.google.cloud.spanner.MetricRegistryConstants.MAX_IN_USE_SESSIONS;
+import static com.google.cloud.spanner.MetricRegistryConstants.METRIC_PREFIX;
+import static com.google.cloud.spanner.MetricRegistryConstants.NUM_ACQUIRED_SESSIONS;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_IN_USE_SESSIONS;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_READ_SESSIONS;
+import static com.google.cloud.spanner.MetricRegistryConstants.NUM_RELEASED_SESSIONS;
+import static com.google.cloud.spanner.MetricRegistryConstants.NUM_SESSIONS_AVAILABLE;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_SESSIONS_BEING_PREPARED;
+import static com.google.cloud.spanner.MetricRegistryConstants.NUM_SESSIONS_IN_POOL;
+import static com.google.cloud.spanner.MetricRegistryConstants.NUM_SESSIONS_IN_USE;
 import static com.google.cloud.spanner.MetricRegistryConstants.NUM_WRITE_SESSIONS;
 import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_DEFAULT_LABEL_VALUES;
 import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_LABEL_KEYS;
@@ -31,8 +40,10 @@
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.atMost;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doThrow;
@@ -74,7 +85,17 @@
 import com.google.spanner.v1.RollbackRequest;
 import io.opencensus.metrics.LabelValue;
 import io.opencensus.metrics.MetricRegistry;
-import io.opencensus.trace.Span;
+import io.opencensus.metrics.Metrics;
+import io.opencensus.trace.Tracing;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
@@ -125,18 +146,32 @@ public class SessionPoolTest extends BaseSessionPoolTest {
   private String sessionName = String.format("%s/sessions/s", db.getName());
   private String TEST_DATABASE_ROLE = "my-role";
 
+  private final TraceWrapper tracer =
+      new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""));
+
   @Parameters(name = "min sessions = {0}")
   public static Collection<Object[]> data() {
     return Arrays.asList(new Object[][] {{0}, {1}});
   }
 
   private SessionPool createPool() {
-    return SessionPool.createPool(options, new TestExecutorFactory(), client.getSessionClient(db));
+    return SessionPool.createPool(
+        options,
+        new TestExecutorFactory(),
+        client.getSessionClient(db),
+        tracer,
+        OpenTelemetry.noop());
   }
 
   private SessionPool createPool(Clock clock) {
     return SessionPool.createPool(
-        options, new TestExecutorFactory(), client.getSessionClient(db), clock, Position.RANDOM);
+        options,
+        new TestExecutorFactory(),
+        client.getSessionClient(db),
+        clock,
+        Position.RANDOM,
+        tracer,
+        OpenTelemetry.noop());
   }
 
   private SessionPool createPool(
@@ -149,7 +184,30 @@ private SessionPool createPool(
         clock,
         Position.RANDOM,
         metricRegistry,
-        labelValues);
+        tracer,
+        labelValues,
+        OpenTelemetry.noop(),
+        null);
+  }
+
+  private SessionPool createPool(
+      Clock clock,
+      MetricRegistry metricRegistry,
+      List<LabelValue> labelValues,
+      OpenTelemetry openTelemetry,
+      Attributes attributes) {
+    return SessionPool.createPool(
+        options,
+        TEST_DATABASE_ROLE,
+        new TestExecutorFactory(),
+        client.getSessionClient(db),
+        clock,
+        Position.RANDOM,
+        metricRegistry,
+        tracer,
+        labelValues,
+        openTelemetry,
+        attributes);
   }
 
   @BeforeClass
@@ -168,6 +226,8 @@ public static void resetLogging() {
   @Before
   public void setUp() {
     initMocks(this);
+    SpannerOptions.resetActiveTracingFramework();
+    SpannerOptions.enableOpenTelemetryTraces();
     when(client.getOptions()).thenReturn(spannerOptions);
     when(client.getSessionClient(db)).thenReturn(sessionClient);
     when(sessionClient.getSpanner()).thenReturn(client);
@@ -1375,19 +1435,27 @@ public void testSessionNotFoundReadWriteTransaction() {
       final SessionImpl closedSession = mock(SessionImpl.class);
       when(closedSession.getName())
           .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-closed");
+
+      Span oTspan = mock(Span.class);
+      ISpan span = new OpenTelemetrySpan(oTspan);
+      when(oTspan.makeCurrent()).thenReturn(mock(Scope.class));
+
       final TransactionContextImpl closedTransactionContext =
           TransactionContextImpl.newBuilder()
               .setSession(closedSession)
               .setOptions(Options.fromTransactionOptions())
               .setRpc(rpc)
+              .setTracer(tracer)
+              .setSpan(span)
               .build();
       when(closedSession.asyncClose())
           .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance()));
       when(closedSession.newTransaction(Options.fromTransactionOptions()))
           .thenReturn(closedTransactionContext);
       when(closedSession.beginTransactionAsync(any(), eq(true))).thenThrow(sessionNotFound);
+      when(closedSession.getTracer()).thenReturn(tracer);
       TransactionRunnerImpl closedTransactionRunner = new TransactionRunnerImpl(closedSession);
-      closedTransactionRunner.setSpan(mock(Span.class));
+      closedTransactionRunner.setSpan(span);
       when(closedSession.readWriteTransaction()).thenReturn(closedTransactionRunner);
 
       final SessionImpl openSession = mock(SessionImpl.class);
@@ -1400,8 +1468,9 @@ public void testSessionNotFoundReadWriteTransaction() {
           .thenReturn(openTransactionContext);
       when(openSession.beginTransactionAsync(any(), eq(true)))
           .thenReturn(ApiFutures.immediateFuture(ByteString.copyFromUtf8("open-txn")));
+      when(openSession.getTracer()).thenReturn(tracer);
       TransactionRunnerImpl openTransactionRunner = new TransactionRunnerImpl(openSession);
-      openTransactionRunner.setSpan(mock(Span.class));
+      openTransactionRunner.setSpan(span);
       when(openSession.readWriteTransaction()).thenReturn(openTransactionRunner);
 
       ResultSet openResultSet = mock(ResultSet.class);
@@ -1455,7 +1524,12 @@ public void testSessionNotFoundReadWriteTransaction() {
       when(spannerOptions.getDatabaseRole()).thenReturn("role");
       when(spanner.getOptions()).thenReturn(spannerOptions);
       SessionPool pool =
-          SessionPool.createPool(options, new TestExecutorFactory(), spanner.getSessionClient(db));
+          SessionPool.createPool(
+              options,
+              new TestExecutorFactory(),
+              spanner.getSessionClient(db),
+              tracer,
+              OpenTelemetry.noop());
       try (PooledSessionFuture readWriteSession = pool.getSession()) {
         TransactionRunner runner = readWriteSession.readWriteTransaction();
         try {
@@ -1554,7 +1628,7 @@ public void testSessionNotFoundWrite() {
     FakeClock clock = new FakeClock();
     clock.currentTimeMillis.set(System.currentTimeMillis());
     pool = createPool(clock);
-    DatabaseClientImpl impl = new DatabaseClientImpl(pool);
+    DatabaseClientImpl impl = new DatabaseClientImpl(pool, tracer);
     assertThat(impl.write(mutations)).isNotNull();
   }
 
@@ -1596,7 +1670,7 @@ public void testSessionNotFoundWriteAtLeastOnce() {
     FakeClock clock = new FakeClock();
     clock.currentTimeMillis.set(System.currentTimeMillis());
     pool = createPool(clock);
-    DatabaseClientImpl impl = new DatabaseClientImpl(pool);
+    DatabaseClientImpl impl = new DatabaseClientImpl(pool, tracer);
     assertThat(impl.writeAtLeastOnce(mutations)).isNotNull();
   }
 
@@ -1635,13 +1709,13 @@ public void testSessionNotFoundPartitionedUpdate() {
     FakeClock clock = new FakeClock();
     clock.currentTimeMillis.set(System.currentTimeMillis());
     pool = createPool(clock);
-    DatabaseClientImpl impl = new DatabaseClientImpl(pool);
+    DatabaseClientImpl impl = new DatabaseClientImpl(pool, mock(TraceWrapper.class));
     assertThat(impl.executePartitionedUpdate(statement)).isEqualTo(1L);
   }
 
   @SuppressWarnings("rawtypes")
   @Test
-  public void testSessionMetrics() throws Exception {
+  public void testOpenCensusSessionMetrics() throws Exception {
     // Create a session pool with max 2 session and a low timeout for waiting for a session.
     options =
         SessionPoolOptions.newBuilder()
@@ -1671,42 +1745,42 @@ public void testSessionMetrics() throws Exception {
     assertThat(record.getMetrics().size()).isEqualTo(6);
 
     List<PointWithFunction> maxInUseSessions =
-        record.getMetrics().get(MetricRegistryConstants.MAX_IN_USE_SESSIONS);
+        record.getMetrics().get(METRIC_PREFIX + MAX_IN_USE_SESSIONS);
     assertThat(maxInUseSessions.size()).isEqualTo(1);
     assertThat(maxInUseSessions.get(0).value()).isEqualTo(2L);
     assertThat(maxInUseSessions.get(0).keys()).isEqualTo(SPANNER_LABEL_KEYS);
     assertThat(maxInUseSessions.get(0).values()).isEqualTo(labelValues);
 
     List<PointWithFunction> getSessionsTimeouts =
-        record.getMetrics().get(MetricRegistryConstants.GET_SESSION_TIMEOUTS);
+        record.getMetrics().get(METRIC_PREFIX + GET_SESSION_TIMEOUTS);
     assertThat(getSessionsTimeouts.size()).isEqualTo(1);
     assertThat(getSessionsTimeouts.get(0).value()).isAtMost(1L);
     assertThat(getSessionsTimeouts.get(0).keys()).isEqualTo(SPANNER_LABEL_KEYS);
     assertThat(getSessionsTimeouts.get(0).values()).isEqualTo(labelValues);
 
     List<PointWithFunction> numAcquiredSessions =
-        record.getMetrics().get(MetricRegistryConstants.NUM_ACQUIRED_SESSIONS);
+        record.getMetrics().get(METRIC_PREFIX + NUM_ACQUIRED_SESSIONS);
     assertThat(numAcquiredSessions.size()).isEqualTo(1);
     assertThat(numAcquiredSessions.get(0).value()).isEqualTo(2L);
     assertThat(numAcquiredSessions.get(0).keys()).isEqualTo(SPANNER_LABEL_KEYS);
     assertThat(numAcquiredSessions.get(0).values()).isEqualTo(labelValues);
 
     List<PointWithFunction> numReleasedSessions =
-        record.getMetrics().get(MetricRegistryConstants.NUM_RELEASED_SESSIONS);
+        record.getMetrics().get(METRIC_PREFIX + NUM_RELEASED_SESSIONS);
     assertThat(numReleasedSessions.size()).isEqualTo(1);
     assertThat(numReleasedSessions.get(0).value()).isEqualTo(0);
     assertThat(numReleasedSessions.get(0).keys()).isEqualTo(SPANNER_LABEL_KEYS);
     assertThat(numReleasedSessions.get(0).values()).isEqualTo(labelValues);
 
     List<PointWithFunction> maxAllowedSessions =
-        record.getMetrics().get(MetricRegistryConstants.MAX_ALLOWED_SESSIONS);
+        record.getMetrics().get(METRIC_PREFIX + MAX_ALLOWED_SESSIONS);
     assertThat(maxAllowedSessions.size()).isEqualTo(1);
     assertThat(maxAllowedSessions.get(0).value()).isEqualTo(options.getMaxSessions());
     assertThat(maxAllowedSessions.get(0).keys()).isEqualTo(SPANNER_LABEL_KEYS);
     assertThat(maxAllowedSessions.get(0).values()).isEqualTo(labelValues);
 
     List<PointWithFunction> numSessionsInPool =
-        record.getMetrics().get(MetricRegistryConstants.NUM_SESSIONS_IN_POOL);
+        record.getMetrics().get(METRIC_PREFIX + NUM_SESSIONS_IN_POOL);
     assertThat(numSessionsInPool.size()).isEqualTo(4);
     PointWithFunction beingPrepared = numSessionsInPool.get(0);
     List<LabelValue> labelValuesWithBeingPreparedType = new ArrayList<>(labelValues);
@@ -1761,19 +1835,19 @@ public void testSessionMetrics() throws Exception {
     executor.shutdown();
 
     session1.close();
-    numAcquiredSessions = record.getMetrics().get(MetricRegistryConstants.NUM_ACQUIRED_SESSIONS);
+    numAcquiredSessions = record.getMetrics().get(METRIC_PREFIX + NUM_ACQUIRED_SESSIONS);
     assertThat(numAcquiredSessions.size()).isEqualTo(1);
     assertThat(numAcquiredSessions.get(0).value()).isEqualTo(3L);
 
-    numReleasedSessions = record.getMetrics().get(MetricRegistryConstants.NUM_RELEASED_SESSIONS);
+    numReleasedSessions = record.getMetrics().get(METRIC_PREFIX + NUM_RELEASED_SESSIONS);
     assertThat(numReleasedSessions.size()).isEqualTo(1);
     assertThat(numReleasedSessions.get(0).value()).isEqualTo(3L);
 
-    maxInUseSessions = record.getMetrics().get(MetricRegistryConstants.MAX_IN_USE_SESSIONS);
+    maxInUseSessions = record.getMetrics().get(METRIC_PREFIX + MAX_IN_USE_SESSIONS);
     assertThat(maxInUseSessions.size()).isEqualTo(1);
     assertThat(maxInUseSessions.get(0).value()).isEqualTo(2L);
 
-    numSessionsInPool = record.getMetrics().get(MetricRegistryConstants.NUM_SESSIONS_IN_POOL);
+    numSessionsInPool = record.getMetrics().get(METRIC_PREFIX + NUM_SESSIONS_IN_POOL);
     assertThat(numSessionsInPool.size()).isEqualTo(4);
     beingPrepared = numSessionsInPool.get(0);
     assertThat(beingPrepared.value()).isEqualTo(0L);
@@ -1785,6 +1859,191 @@ public void testSessionMetrics() throws Exception {
     assertThat(writePreparedSessions.value()).isEqualTo(0L);
   }
 
+  @Test
+  public void testOpenCensusMetricsDisable() {
+    SpannerOptions.disableOpenCensusMetrics();
+    // Create a session pool with max 2 session and a low timeout for waiting for a session.
+    options =
+        SessionPoolOptions.newBuilder()
+            .setMinSessions(1)
+            .setMaxSessions(2)
+            .setMaxIdleSessions(0)
+            .setInitialWaitForSessionTimeoutMillis(50L)
+            .build();
+    FakeClock clock = new FakeClock();
+    clock.currentTimeMillis.set(System.currentTimeMillis());
+    FakeMetricRegistry metricRegistry = new FakeMetricRegistry();
+    List<LabelValue> labelValues =
+        Arrays.asList(
+            LabelValue.create("client1"),
+            LabelValue.create("database1"),
+            LabelValue.create("instance1"),
+            LabelValue.create("1.0.0"));
+
+    setupMockSessionCreation();
+    pool = createPool(clock, metricRegistry, labelValues);
+    PooledSessionFuture session1 = pool.getSession();
+    PooledSessionFuture session2 = pool.getSession();
+    session1.get();
+    session2.get();
+
+    MetricsRecord record = metricRegistry.pollRecord();
+    assertThat(record.getMetrics().size()).isEqualTo(0);
+    SpannerOptions.enableOpenCensusMetrics();
+  }
+
+  @Test
+  public void testOpenTelemetrySessionMetrics() throws Exception {
+    SpannerOptions.resetActiveTracingFramework();
+    SpannerOptions.enableOpenTelemetryMetrics();
+    // Create a session pool with max 2 session and a low timeout for waiting for a session.
+    if (minSessions == 1) {
+      options =
+          SessionPoolOptions.newBuilder()
+              .setMinSessions(1)
+              .setMaxSessions(3)
+              .setMaxIdleSessions(0)
+              .setInitialWaitForSessionTimeoutMillis(50L)
+              .build();
+      FakeClock clock = new FakeClock();
+      clock.currentTimeMillis.set(System.currentTimeMillis());
+
+      InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+      SdkMeterProvider sdkMeterProvider =
+          SdkMeterProvider.builder().registerMetricReader(inMemoryMetricReader).build();
+      OpenTelemetry openTelemetry =
+          OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProvider).build();
+
+      setupMockSessionCreation();
+
+      AttributesBuilder attributesBuilder = Attributes.builder();
+      attributesBuilder.put("client_id", "testClient");
+      attributesBuilder.put("database", "testDb");
+      attributesBuilder.put("instance_id", "test_instance");
+      attributesBuilder.put("library_version", "test_version");
+
+      pool =
+          createPool(
+              clock,
+              Metrics.getMetricRegistry(),
+              SPANNER_DEFAULT_LABEL_VALUES,
+              openTelemetry,
+              attributesBuilder.build());
+      PooledSessionFuture session1 = pool.getSession();
+      PooledSessionFuture session2 = pool.getSession();
+      session1.get();
+      session2.get();
+
+      Collection<MetricData> metricDataCollection = inMemoryMetricReader.collectAllMetrics();
+      // Acquired sessions are 2.
+      verifyMetricData(metricDataCollection, NUM_ACQUIRED_SESSIONS, 1, 2L);
+      // Max in use session are 2.
+      verifyMetricData(metricDataCollection, MAX_IN_USE_SESSIONS, 1, 2D);
+      // Max Allowed sessions should be 3
+      verifyMetricData(metricDataCollection, MAX_ALLOWED_SESSIONS, 1, 3D);
+      // Released sessions should be 0
+      verifyMetricData(metricDataCollection, NUM_RELEASED_SESSIONS, 1, 0L);
+      // Num sessions in pool
+      verifyMetricData(metricDataCollection, NUM_SESSIONS_IN_POOL, 1, NUM_SESSIONS_IN_USE, 2);
+
+      PooledSessionFuture session3 = pool.getSession();
+      session3.get();
+
+      final CountDownLatch latch = new CountDownLatch(1);
+      // Try asynchronously to take another session. This attempt should time out.
+      Future<Void> fut =
+          executor.submit(
+              () -> {
+                latch.countDown();
+                Session session = pool.getSession();
+                session.close();
+                return null;
+              });
+      // Wait until the background thread is actually waiting for a session.
+      latch.await();
+      // Wait until the request has timed out.
+      int waitCount = 0;
+      while (pool.getNumWaiterTimeouts() == 0L && waitCount < 1000) {
+        Thread.sleep(5L);
+        waitCount++;
+      }
+      // Return the checked out session to the pool so the async request will get a session and
+      // finish.
+      session2.close();
+      // Verify that the async request also succeeds.
+      fut.get(10L, TimeUnit.SECONDS);
+      executor.shutdown();
+
+      metricDataCollection = inMemoryMetricReader.collectAllMetrics();
+
+      // Max Allowed sessions should be 3
+      verifyMetricData(metricDataCollection, MAX_ALLOWED_SESSIONS, 1, 3D);
+      // Session timeouts 1
+      // verifyMetricData(metricDataCollection, GET_SESSION_TIMEOUTS, 1, 1L);
+      // Max in use session are 2.
+      verifyMetricData(metricDataCollection, MAX_IN_USE_SESSIONS, 1, 3D);
+      // Session released 2
+      verifyMetricData(metricDataCollection, NUM_RELEASED_SESSIONS, 1, 2L);
+      // Acquired sessions are 4.
+      verifyMetricData(metricDataCollection, NUM_ACQUIRED_SESSIONS, 1, 4L);
+      // Num sessions in pool
+      verifyMetricData(metricDataCollection, NUM_SESSIONS_IN_POOL, 1, NUM_SESSIONS_IN_USE, 2);
+      verifyMetricData(metricDataCollection, NUM_SESSIONS_IN_POOL, 1, NUM_SESSIONS_AVAILABLE, 1);
+    }
+  }
+
+  private static void verifyMetricData(
+      Collection<MetricData> metricDataCollection, String metricName, int size, long value) {
+    Collection<MetricData> metricDataFiltered =
+        metricDataCollection.stream()
+            .filter(x -> x.getName().equals(metricName))
+            .collect(Collectors.toList());
+
+    assertEquals(metricDataFiltered.size(), size);
+    MetricData metricData = metricDataFiltered.stream().findFirst().get();
+    assertEquals(
+        metricData.getLongSumData().getPoints().stream().findFirst().get().getValue(), value);
+  }
+
+  private static void verifyMetricData(
+      Collection<MetricData> metricDataCollection, String metricName, int size, double value) {
+    Collection<MetricData> metricDataFiltered =
+        metricDataCollection.stream()
+            .filter(x -> x.getName().equals(metricName))
+            .collect(Collectors.toList());
+
+    assertEquals(metricDataFiltered.size(), size);
+    MetricData metricData = metricDataFiltered.stream().findFirst().get();
+    assertEquals(
+        metricData.getDoubleGaugeData().getPoints().stream().findFirst().get().getValue(),
+        value,
+        0.0);
+  }
+
+  private static void verifyMetricData(
+      Collection<MetricData> metricDataCollection,
+      String metricName,
+      int size,
+      String labelName,
+      long value) {
+    Collection<MetricData> metricDataFiltered =
+        metricDataCollection.stream()
+            .filter(x -> x.getName().equals(metricName))
+            .collect(Collectors.toList());
+
+    assertEquals(metricDataFiltered.size(), size);
+
+    MetricData metricData = metricDataFiltered.stream().findFirst().get();
+
+    assertEquals(
+        metricData.getLongSumData().getPoints().stream()
+            .filter(x -> x.getAttributes().asMap().containsValue(labelName))
+            .findFirst()
+            .get()
+            .getValue(),
+        value);
+  }
+
   @Test
   public void testGetDatabaseRole() throws Exception {
     setupMockSessionCreation();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java
index 4d13829a857..672827318d1 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java
@@ -25,6 +25,7 @@
 import com.google.cloud.NoCredentials;
 import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime;
 import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.common.collect.ImmutableList;
 import com.google.protobuf.ListValue;
 import com.google.spanner.v1.ResultSetMetadata;
 import com.google.spanner.v1.StructType;
@@ -35,9 +36,19 @@
 import io.grpc.StatusRuntimeException;
 import io.grpc.inprocess.InProcessServerBuilder;
 import io.opencensus.trace.Tracing;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
+import io.opentelemetry.context.propagation.ContextPropagators;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
 import java.lang.reflect.Modifier;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.stream.Collectors;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Assume;
@@ -90,6 +101,9 @@ public class SpanTest {
   private DatabaseClient client;
   private Spanner spannerWithTimeout;
   private DatabaseClient clientWithTimeout;
+
+  private static InMemorySpanExporter openTelemetrySpanExporter;
+
   private static FailOnOverkillTraceComponentImpl failOnOverkillTraceComponent =
       new FailOnOverkillTraceComponentImpl();
 
@@ -152,13 +166,36 @@ public static void stopServer() throws InterruptedException {
     }
   }
 
+  @BeforeClass
+  public static void setupOpenTelemetry() {
+    SpannerOptions.resetActiveTracingFramework();
+    SpannerOptions.enableOpenCensusTraces();
+  }
+
   @Before
   public void setUp() throws Exception {
+    // Incorporating OpenTelemetry configuration to ensure that OpenCensus traces are utilized by
+    // default,
+    // regardless of the presence of OpenTelemetry configuration.
+    openTelemetrySpanExporter = InMemorySpanExporter.create();
+    SdkTracerProvider tracerProvider =
+        SdkTracerProvider.builder()
+            .addSpanProcessor(SimpleSpanProcessor.create(openTelemetrySpanExporter))
+            .build();
+
+    GlobalOpenTelemetry.resetForTest();
+    OpenTelemetry openTelemetry =
+        OpenTelemetrySdk.builder()
+            .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
+            .setTracerProvider(tracerProvider)
+            .build();
+
     SpannerOptions.Builder builder =
         SpannerOptions.newBuilder()
             .setProjectId(TEST_PROJECT)
             .setChannelProvider(channelProvider)
             .setCredentials(NoCredentials.getInstance())
+            .setOpenTelemetry(openTelemetry)
             .setSessionPoolOption(SessionPoolOptions.newBuilder().setMinSessions(0).build());
 
     spanner = builder.build().getService();
@@ -206,6 +243,7 @@ public void setUp() throws Exception {
             DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
 
     failOnOverkillTraceComponent.clearSpans();
+    failOnOverkillTraceComponent.clearAnnotations();
   }
 
   @After
@@ -240,12 +278,30 @@ public void singleUse() {
         // Just consume the result set.
       }
     }
+
+    // OpenTelemetry spans should be 0 as OpenCensus is default enabled.
+    assertEquals(openTelemetrySpanExporter.getFinishedSpanItems().size(), 0);
+
+    // OpenCensus spans and events verification
     Map<String, Boolean> spans = failOnOverkillTraceComponent.getSpans();
     assertThat(spans).containsEntry("CloudSpanner.ReadOnlyTransaction", true);
     assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessions", true);
     assertThat(spans).containsEntry("SessionPool.WaitForSession", true);
     assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true);
     assertThat(spans).containsEntry("CloudSpannerOperation.ExecuteStreamingQuery", true);
+
+    List<String> expectedAnnotations =
+        ImmutableList.of(
+            "Requesting 25 sessions",
+            "Request for 25 sessions returned 25 sessions",
+            "Creating 25 sessions",
+            "Acquiring session",
+            "No session available",
+            "Creating sessions",
+            "Waiting for a session to come available",
+            "Using Session",
+            "Starting/Resuming stream");
+    verifyAnnotations(failOnOverkillTraceComponent.getAnnotations(), expectedAnnotations);
   }
 
   @Test
@@ -264,6 +320,21 @@ public void multiUse() {
     assertThat(spans).containsEntry("SessionPool.WaitForSession", true);
     assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true);
     assertThat(spans).containsEntry("CloudSpannerOperation.ExecuteStreamingQuery", true);
+
+    List<String> expectedAnnotations =
+        ImmutableList.of(
+            "Requesting 25 sessions",
+            "Request for 25 sessions returned 25 sessions",
+            "Creating 25 sessions",
+            "Acquiring session",
+            "No session available",
+            "Creating sessions",
+            "Waiting for a session to come available",
+            "Using Session",
+            "Starting/Resuming stream",
+            "Creating Transaction",
+            "Transaction Creation Done");
+    verifyAnnotations(failOnOverkillTraceComponent.getAnnotations(), expectedAnnotations);
   }
 
   @Test
@@ -276,6 +347,22 @@ public void transactionRunner() {
     assertThat(spans).containsEntry("SessionPool.WaitForSession", true);
     assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true);
     assertThat(spans).containsEntry("CloudSpannerOperation.Commit", true);
+
+    List<String> expectedAnnotations =
+        ImmutableList.of(
+            "Acquiring session",
+            "No session available",
+            "Creating sessions",
+            "Waiting for a session to come available",
+            "Using Session",
+            "Starting Transaction Attempt",
+            "Starting Commit",
+            "Commit Done",
+            "Transaction Attempt Succeeded",
+            "Requesting 25 sessions",
+            "Request for 25 sessions returned 25 sessions",
+            "Creating 25 sessions");
+    verifyAnnotations(expectedAnnotations, failOnOverkillTraceComponent.getAnnotations());
   }
 
   @Test
@@ -293,5 +380,26 @@ public void transactionRunnerWithError() {
     assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessions", true);
     assertThat(spans).containsEntry("SessionPool.WaitForSession", true);
     assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true);
+
+    List<String> expectedAnnotations =
+        ImmutableList.of(
+            "Acquiring session",
+            "No session available",
+            "Creating sessions",
+            "Waiting for a session to come available",
+            "Using Session",
+            "Starting Transaction Attempt",
+            "Transaction Attempt Failed in user operation",
+            "Requesting 25 sessions",
+            "Request for 25 sessions returned 25 sessions",
+            "Creating 25 sessions");
+
+    verifyAnnotations(expectedAnnotations, failOnOverkillTraceComponent.getAnnotations());
+  }
+
+  private void verifyAnnotations(List<String> actualAnnotations, List<String> expectedAnnotations) {
+    assertEquals(
+        actualAnnotations.stream().distinct().sorted().collect(Collectors.toList()),
+        expectedAnnotations.stream().sorted().collect(Collectors.toList()));
   }
 }
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java
index 2d098d97004..31a6cad4c8a 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java
@@ -31,6 +31,7 @@
 import com.google.cloud.spanner.SpannerImpl.ClosedException;
 import com.google.cloud.spanner.spi.v1.SpannerRpc;
 import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
+import io.opentelemetry.api.OpenTelemetry;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.Collections;
@@ -39,6 +40,7 @@
 import java.util.UUID;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -57,6 +59,12 @@ public class SpannerImplTest {
 
   @Captor ArgumentCaptor<Map<SpannerRpc.Option, Object>> options;
 
+  @BeforeClass
+  public static void setupOpenTelemetry() {
+    SpannerOptions.resetActiveTracingFramework();
+    SpannerOptions.enableOpenTelemetryTraces();
+  }
+
   @Before
   public void setUp() {
     MockitoAnnotations.initMocks(this);
@@ -66,6 +74,7 @@ public void setUp() {
     when(spannerOptions.getRetrySettings()).thenReturn(RetrySettings.newBuilder().build());
     when(spannerOptions.getClock()).thenReturn(NanoClock.getDefaultClock());
     when(spannerOptions.getSessionLabels()).thenReturn(Collections.emptyMap());
+    when(spannerOptions.getOpenTelemetry()).thenReturn(OpenTelemetry.noop());
     impl = new SpannerImpl(rpc, spannerOptions);
   }
 
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java
index 4e41660bca1..522cd0147a3 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java
@@ -61,6 +61,11 @@
 import com.google.spanner.v1.ReadRequest;
 import com.google.spanner.v1.RollbackRequest;
 import com.google.spanner.v1.SpannerGrpc;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -117,17 +122,24 @@ public void builder() {
     String projectId = "test-project";
     Map<String, String> labels = new HashMap<>();
     labels.put("env", "dev");
+    InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+    SdkMeterProvider sdkMeterProvider =
+        SdkMeterProvider.builder().registerMetricReader(inMemoryMetricReader).build();
+    OpenTelemetry openTelemetry =
+        OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProvider).build();
     SpannerOptions options =
         SpannerOptions.newBuilder()
             .setHost(host)
             .setProjectId(projectId)
             .setPrefetchChunks(2)
             .setSessionLabels(labels)
+            .setOpenTelemetry(openTelemetry)
             .build();
     assertThat(options.getHost()).isEqualTo(host);
     assertThat(options.getProjectId()).isEqualTo(projectId);
     assertThat(options.getPrefetchChunks()).isEqualTo(2);
     assertThat(options.getSessionLabels()).containsExactlyEntriesIn(labels);
+    assertThat(options.getOpenTelemetry()).isEqualTo(openTelemetry);
   }
 
   @Test
@@ -1101,4 +1113,19 @@ public void checkCreatedInstanceWhenGrpcGcpExtensionEnabled() {
     spanner1.close();
     spanner2.close();
   }
+
+  @Test
+  public void checkGlobalOpenTelemetryWhenNotInjected() {
+    GlobalOpenTelemetry.resetForTest();
+    InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+    SdkMeterProvider sdkMeterProvider =
+        SdkMeterProvider.builder().registerMetricReader(inMemoryMetricReader).build();
+    OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProvider).buildAndRegisterGlobal();
+    SpannerOptions options =
+        SpannerOptions.newBuilder()
+            .setProjectId("test-project")
+            .setCredentials(NoCredentials.getInstance())
+            .build();
+    assertEquals(GlobalOpenTelemetry.get(), options.getOpenTelemetry());
+  }
 }
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionContextImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionContextImplTest.java
index 0cb2b5d29ae..53120eda441 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionContextImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionContextImplTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertThrows;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyMap;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -49,6 +50,9 @@ public class TransactionContextImplTest {
 
   @Mock private SessionImpl session;
 
+  @Mock private ISpan span;
+  @Mock private TraceWrapper tracer;
+
   @SuppressWarnings("unchecked")
   @Before
   public void setup() {
@@ -60,12 +64,18 @@ public void setup() {
                     .setCommitTimestamp(Timestamp.newBuilder().setSeconds(99L).setNanos(10).build())
                     .build()));
     when(session.getName()).thenReturn("test");
+    doNothing().when(span).setStatus(any(Throwable.class));
+    doNothing().when(span).end();
+    doNothing().when(span).addAnnotation("Starting Commit");
+    when(tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span)).thenReturn(span);
   }
 
   private TransactionContextImpl createContext() {
     return TransactionContextImpl.newBuilder()
         .setSession(session)
         .setRpc(rpc)
+        .setSpan(span)
+        .setTracer(tracer)
         .setTransactionId(ByteString.copyFromUtf8("test"))
         .setOptions(Options.fromTransactionOptions())
         .build();
@@ -165,6 +175,8 @@ public void testReturnCommitStats() {
         TransactionContextImpl.newBuilder()
             .setSession(session)
             .setRpc(rpc)
+            .setSpan(span)
+            .setTracer(tracer)
             .setTransactionId(transactionId)
             .setOptions(Options.fromTransactionOptions(Options.commitStats()))
             .build()) {
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java
index f8462693fe6..dc28b333c4f 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
@@ -45,13 +46,14 @@
 import com.google.spanner.v1.ResultSetStats;
 import com.google.spanner.v1.Session;
 import com.google.spanner.v1.Transaction;
-import io.opencensus.trace.Span;
+import io.opentelemetry.api.OpenTelemetry;
 import java.util.Collections;
 import java.util.UUID;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -77,10 +79,20 @@ public void release(ScheduledExecutorService exec) {
   @Mock TransactionRunnerImpl.TransactionContextImpl txn;
   private TransactionManagerImpl manager;
 
+  @BeforeClass
+  public static void setupOpenTelemetry() {
+    SpannerOptions.resetActiveTracingFramework();
+    SpannerOptions.enableOpenTelemetryTraces();
+  }
+
   @Before
   public void setUp() {
     initMocks(this);
-    manager = new TransactionManagerImpl(session, mock(Span.class));
+    manager =
+        new TransactionManagerImpl(
+            session,
+            new OpenTelemetrySpan(mock(io.opentelemetry.api.trace.Span.class)),
+            mock(TraceWrapper.class));
   }
 
   @Test
@@ -200,6 +212,7 @@ public void usesPreparedTransaction() {
     when(options.getSessionPoolOptions()).thenReturn(sessionPoolOptions);
     when(options.getSessionLabels()).thenReturn(Collections.emptyMap());
     when(options.getDatabaseRole()).thenReturn("role");
+    when(options.getOpenTelemetry()).thenReturn(OpenTelemetry.noop());
     SpannerRpc rpc = mock(SpannerRpc.class);
     when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap()))
         .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance()));
@@ -262,6 +275,7 @@ public void inlineBegin() {
     when(options.getSessionLabels()).thenReturn(Collections.emptyMap());
     when(options.getDefaultQueryOptions(Mockito.any(DatabaseId.class)))
         .thenReturn(QueryOptions.getDefaultInstance());
+    when(options.getOpenTelemetry()).thenReturn(OpenTelemetry.noop());
     SpannerRpc rpc = mock(SpannerRpc.class);
     when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap()))
         .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance()));
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java
index efc57fb480b..cf5f901f65e 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
@@ -58,7 +59,10 @@
 import io.grpc.Status;
 import io.grpc.StatusRuntimeException;
 import io.grpc.protobuf.ProtoUtils;
-import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracing;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Scope;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.UUID;
@@ -66,6 +70,7 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -95,12 +100,22 @@ public void release(ScheduledExecutorService exec) {
   private TransactionRunnerImpl transactionRunner;
   private boolean firstRun;
   private boolean usedInlinedBegin;
+  private TraceWrapper tracer;
+  private ISpan span;
+
+  @BeforeClass
+  public static void setupOpenTelemetry() {
+    SpannerOptions.resetActiveTracingFramework();
+    SpannerOptions.enableOpenTelemetryTraces();
+  }
 
   @Before
   public void setUp() {
     MockitoAnnotations.initMocks(this);
+    tracer = new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""));
     firstRun = true;
     when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(txn);
+    when(session.getTracer()).thenReturn(tracer);
     when(rpc.executeQuery(Mockito.any(ExecuteSqlRequest.class), Mockito.anyMap(), eq(true)))
         .thenAnswer(
             invocation -> {
@@ -128,7 +143,10 @@ public void setUp() {
                     .build()));
     when(rpc.rollbackAsync(Mockito.any(RollbackRequest.class), Mockito.anyMap()))
         .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance()));
-    transactionRunner.setSpan(mock(Span.class));
+    Span oTspan = mock(Span.class);
+    span = new OpenTelemetrySpan(oTspan);
+    when(oTspan.makeCurrent()).thenReturn(mock(Scope.class));
+    transactionRunner.setSpan(span);
   }
 
   @SuppressWarnings("unchecked")
@@ -144,6 +162,7 @@ public void usesPreparedTransaction() {
     when(options.getSessionPoolOptions()).thenReturn(sessionPoolOptions);
     when(options.getSessionLabels()).thenReturn(Collections.emptyMap());
     when(options.getDatabaseRole()).thenReturn("role");
+    when(options.getOpenTelemetry()).thenReturn(OpenTelemetry.noop());
     SpannerRpc rpc = mock(SpannerRpc.class);
     when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap()))
         .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance()));
@@ -279,6 +298,7 @@ public void inlineBegin() {
     when(spanner.getDefaultQueryOptions(Mockito.any(DatabaseId.class)))
         .thenReturn(QueryOptions.getDefaultInstance());
     when(spanner.getOptions()).thenReturn(options);
+    when(spanner.getTracer()).thenReturn(tracer);
     SessionPoolOptions sessionPoolOptions = SessionPoolOptions.newBuilder().build();
     when(options.getSessionPoolOptions()).thenReturn(sessionPoolOptions);
 
@@ -292,9 +312,9 @@ public void prepareReadWriteTransaction() {
             throw new IllegalStateException();
           }
         };
-    session.setCurrentSpan(mock(Span.class));
+    session.setCurrentSpan(new OpenTelemetrySpan(mock(io.opentelemetry.api.trace.Span.class)));
     TransactionRunnerImpl runner = new TransactionRunnerImpl(session);
-    runner.setSpan(mock(Span.class));
+    runner.setSpan(span);
     assertThat(usedInlinedBegin).isFalse();
     runner.run(
         transaction -> {
@@ -318,6 +338,8 @@ private long[] batchDmlException(int status) {
             .setTransactionId(ByteString.copyFromUtf8(UUID.randomUUID().toString()))
             .setOptions(Options.fromTransactionOptions())
             .setRpc(rpc)
+            .setTracer(session.getTracer())
+            .setSpan(session.getTracer().getCurrentSpan())
             .build();
     when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(transaction);
     when(session.beginTransactionAsync(true))
@@ -325,7 +347,7 @@ private long[] batchDmlException(int status) {
             ApiFutures.immediateFuture(ByteString.copyFromUtf8(UUID.randomUUID().toString())));
     when(session.getName()).thenReturn(SessionId.of("p", "i", "d", "test").getName());
     TransactionRunnerImpl runner = new TransactionRunnerImpl(session);
-    runner.setSpan(mock(Span.class));
+    runner.setSpan(span);
     ExecuteBatchDmlResponse response1 =
         ExecuteBatchDmlResponse.newBuilder()
             .addResultSets(
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GfeLatencyTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GfeLatencyTest.java
index dd6006078eb..9bb09aace70 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GfeLatencyTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GfeLatencyTest.java
@@ -17,7 +17,6 @@
 package com.google.cloud.spanner.spi.v1;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assume.assumeFalse;
 
 import com.google.auth.oauth2.AccessToken;
 import com.google.auth.oauth2.OAuth2Credentials;
@@ -28,7 +27,6 @@
 import com.google.cloud.spanner.Spanner;
 import com.google.cloud.spanner.SpannerOptions;
 import com.google.cloud.spanner.Statement;
-import com.google.cloud.spanner.testing.EmulatorSpannerHelper;
 import com.google.protobuf.ListValue;
 import com.google.spanner.v1.ResultSetMetadata;
 import com.google.spanner.v1.StructType;
@@ -138,7 +136,6 @@ public class GfeLatencyTest {
   @BeforeClass
   public static void startServer() throws IOException {
     SpannerRpcViews.registerGfeLatencyAndHeaderMissingCountViews();
-    assumeFalse(EmulatorSpannerHelper.isUsingEmulator());
 
     mockSpanner = new MockSpannerServiceImpl();
     mockSpanner.setAbortProbability(0.0D); // We don't want any unpredictable aborted transactions.
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/SpannerRpcMetricsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/SpannerRpcMetricsTest.java
new file mode 100644
index 00000000000..049ce0d1960
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/SpannerRpcMetricsTest.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2023 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
+ *
+ *      https://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.spi.v1;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.cloud.NoCredentials;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.MockSpannerServiceImpl;
+import com.google.cloud.spanner.Spanner;
+import com.google.cloud.spanner.SpannerOptions;
+import com.google.cloud.spanner.Statement;
+import com.google.protobuf.ListValue;
+import com.google.spanner.v1.ResultSetMetadata;
+import com.google.spanner.v1.StructType;
+import com.google.spanner.v1.TypeCode;
+import io.grpc.ForwardingServerCall;
+import io.grpc.Metadata;
+import io.grpc.Server;
+import io.grpc.ServerCall;
+import io.grpc.ServerCallHandler;
+import io.grpc.ServerInterceptor;
+import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SpannerRpcMetricsTest {
+
+  private static MockSpannerServiceImpl mockSpanner;
+  private static Server server;
+  private static InetSocketAddress address;
+  private static Spanner spannerWithOpenTelemetry;
+  private static DatabaseClient databaseClient;
+  private static final Map<SpannerRpc.Option, Object> optionsMap = new HashMap<>();
+  private static MockSpannerServiceImpl mockSpannerNoHeader;
+  private static Server serverNoHeader;
+  private static InetSocketAddress addressNoHeader;
+  private static Spanner spannerNoHeaderNoOpenTelemetry;
+  private static DatabaseClient databaseClientNoHeader;
+  private static String instanceId = "fake-instance";
+  private static String databaseId = "fake-database";
+  private static String projectId = "fake-project";
+  private static AtomicInteger fakeServerTiming = new AtomicInteger(new Random().nextInt(1000) + 1);
+  private static final Statement SELECT1AND2 =
+      Statement.of("SELECT 1 AS COL1 UNION ALL SELECT 2 AS COL1");
+  private static final ResultSetMetadata SELECT1AND2_METADATA =
+      ResultSetMetadata.newBuilder()
+          .setRowType(
+              StructType.newBuilder()
+                  .addFields(
+                      StructType.Field.newBuilder()
+                          .setName("COL1")
+                          .setType(
+                              com.google.spanner.v1.Type.newBuilder()
+                                  .setCode(TypeCode.INT64)
+                                  .build())
+                          .build())
+                  .build())
+          .build();
+  private static final com.google.spanner.v1.ResultSet SELECT1_RESULTSET =
+      com.google.spanner.v1.ResultSet.newBuilder()
+          .addRows(
+              ListValue.newBuilder()
+                  .addValues(com.google.protobuf.Value.newBuilder().setStringValue("1").build())
+                  .build())
+          .addRows(
+              ListValue.newBuilder()
+                  .addValues(com.google.protobuf.Value.newBuilder().setStringValue("2").build())
+                  .build())
+          .setMetadata(SELECT1AND2_METADATA)
+          .build();
+  private static final Statement UPDATE_FOO_STATEMENT =
+      Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2");
+
+  private static InMemoryMetricReader inMemoryMetricReader;
+
+  private static InMemoryMetricReader inMemoryMetricReaderInjected;
+
+  @BeforeClass
+  public static void startServer() throws IOException {
+    SpannerOptions.enableOpenTelemetryMetrics();
+    mockSpanner = new MockSpannerServiceImpl();
+    mockSpanner.setAbortProbability(0.0D); // We don't want any unpredictable aborted transactions.
+    mockSpanner.putStatementResult(
+        MockSpannerServiceImpl.StatementResult.query(SELECT1AND2, SELECT1_RESULTSET));
+    mockSpanner.putStatementResult(
+        MockSpannerServiceImpl.StatementResult.update(UPDATE_FOO_STATEMENT, 1L));
+    address = new InetSocketAddress("localhost", 0);
+    server =
+        NettyServerBuilder.forAddress(address)
+            .addService(mockSpanner)
+            .intercept(
+                new ServerInterceptor() {
+                  @Override
+                  public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
+                      ServerCall<ReqT, RespT> serverCall,
+                      Metadata headers,
+                      ServerCallHandler<ReqT, RespT> serverCallHandler) {
+                    return serverCallHandler.startCall(
+                        new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(
+                            serverCall) {
+                          @Override
+                          public void sendHeaders(Metadata headers) {
+                            headers.put(
+                                Metadata.Key.of("server-timing", Metadata.ASCII_STRING_MARSHALLER),
+                                String.format("gfet4t7; dur=%d", fakeServerTiming.get()));
+                            super.sendHeaders(headers);
+                          }
+                        },
+                        headers);
+                  }
+                })
+            .build()
+            .start();
+    optionsMap.put(SpannerRpc.Option.CHANNEL_HINT, 1L);
+    inMemoryMetricReader = InMemoryMetricReader.create();
+    SdkMeterProvider sdkMeterProvider =
+        SdkMeterProvider.builder().registerMetricReader(inMemoryMetricReader).build();
+    GlobalOpenTelemetry.resetForTest();
+    OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProvider).buildAndRegisterGlobal();
+
+    inMemoryMetricReaderInjected = InMemoryMetricReader.create();
+    SdkMeterProvider sdkMeterProviderInjected =
+        SdkMeterProvider.builder().registerMetricReader(inMemoryMetricReaderInjected).build();
+
+    OpenTelemetry openTelemetry =
+        OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProviderInjected).build();
+
+    spannerWithOpenTelemetry =
+        createSpannerOptionsWithOpenTelemetry(address, server, openTelemetry).getService();
+    databaseClient =
+        spannerWithOpenTelemetry.getDatabaseClient(
+            DatabaseId.of(projectId, instanceId, databaseId));
+
+    mockSpannerNoHeader = new MockSpannerServiceImpl();
+    mockSpannerNoHeader.setAbortProbability(0.0D);
+    mockSpannerNoHeader.putStatementResult(
+        MockSpannerServiceImpl.StatementResult.query(SELECT1AND2, SELECT1_RESULTSET));
+    mockSpannerNoHeader.putStatementResult(
+        MockSpannerServiceImpl.StatementResult.update(UPDATE_FOO_STATEMENT, 1L));
+    addressNoHeader = new InetSocketAddress("localhost", 0);
+    serverNoHeader =
+        NettyServerBuilder.forAddress(addressNoHeader)
+            .addService(mockSpannerNoHeader)
+            .build()
+            .start();
+    spannerNoHeaderNoOpenTelemetry =
+        createSpannerOptions(addressNoHeader, serverNoHeader).getService();
+    databaseClientNoHeader =
+        spannerNoHeaderNoOpenTelemetry.getDatabaseClient(
+            DatabaseId.of(projectId, instanceId, databaseId));
+  }
+
+  @AfterClass
+  public static void stopServer() throws InterruptedException {
+    if (spannerWithOpenTelemetry != null) {
+      spannerWithOpenTelemetry.close();
+      server.shutdown();
+      server.awaitTermination();
+    }
+
+    if (spannerNoHeaderNoOpenTelemetry != null) {
+      spannerNoHeaderNoOpenTelemetry.close();
+      serverNoHeader.shutdown();
+      serverNoHeader.awaitTermination();
+    }
+  }
+
+  @After
+  public void reset() {
+    mockSpanner.reset();
+    mockSpannerNoHeader.reset();
+  }
+
+  @Test
+  public void testGfeLatencyExecuteSqlWithInjectedOpenTelemetry() throws InterruptedException {
+    databaseClient
+        .readWriteTransaction()
+        .run(transaction -> transaction.executeUpdate(UPDATE_FOO_STATEMENT));
+
+    double latency =
+        getGfeLatencyMetric(
+            getMetricData("spanner/gfe_latency", inMemoryMetricReaderInjected),
+            "google.spanner.v1.Spanner/ExecuteSql");
+    assertEquals(fakeServerTiming.get(), latency, 0);
+  }
+
+  @Test
+  public void testGfeMissingHeaderExecuteSqlWithGlobalOpenTelemetry() throws InterruptedException {
+    databaseClient
+        .readWriteTransaction()
+        .run(transaction -> transaction.executeUpdate(UPDATE_FOO_STATEMENT));
+    long count =
+        getHeaderLatencyMetric(
+            getMetricData("spanner/gfe_header_missing_count", inMemoryMetricReaderInjected),
+            "google.spanner.v1.Spanner/ExecuteSql");
+    assertEquals(0, count);
+
+    databaseClientNoHeader
+        .readWriteTransaction()
+        .run(transaction -> transaction.executeUpdate(UPDATE_FOO_STATEMENT));
+    long count1 =
+        getHeaderLatencyMetric(
+            getMetricData("spanner/gfe_header_missing_count", inMemoryMetricReader),
+            "google.spanner.v1.Spanner/ExecuteSql");
+    assertEquals(1, count1);
+  }
+
+  private static SpannerOptions createSpannerOptionsWithOpenTelemetry(
+      InetSocketAddress address, Server server, OpenTelemetry openTelemetry) {
+
+    String endpoint = address.getHostString() + ":" + server.getPort();
+    return SpannerOptions.newBuilder()
+        .setProjectId("[PROJECT]")
+        .setChannelConfigurator(
+            input -> {
+              input.usePlaintext();
+              return input;
+            })
+        .setHost("http://" + endpoint)
+        .setCredentials(NoCredentials.getInstance())
+        .setOpenTelemetry(openTelemetry)
+        .build();
+  }
+
+  private static SpannerOptions createSpannerOptions(InetSocketAddress address, Server server) {
+
+    String endpoint = address.getHostString() + ":" + server.getPort();
+    return SpannerOptions.newBuilder()
+        .setProjectId("[PROJECT]")
+        .setChannelConfigurator(
+            input -> {
+              input.usePlaintext();
+              return input;
+            })
+        .setHost("http://" + endpoint)
+        .setCredentials(NoCredentials.getInstance())
+        .build();
+  }
+
+  private long getHeaderLatencyMetric(MetricData metricData, String methodName) {
+    return metricData.getLongSumData().getPoints().stream()
+        .filter(x -> x.getAttributes().asMap().containsValue(methodName))
+        .findFirst()
+        .get()
+        .getValue();
+  }
+
+  private double getGfeLatencyMetric(MetricData metricData, String methodName) {
+    return metricData.getHistogramData().getPoints().stream()
+        .filter(x -> x.getAttributes().asMap().containsValue(methodName))
+        .findFirst()
+        .get()
+        .getMax();
+  }
+
+  private MetricData getMetricData(String metricName, InMemoryMetricReader inMemoryMetricReader) {
+    Collection<MetricData> metricDataCollection = inMemoryMetricReader.collectAllMetrics();
+    Collection<MetricData> metricDataFiltered =
+        metricDataCollection.stream()
+            .filter(x -> x.getName().equals(metricName))
+            .collect(Collectors.toList());
+    return metricDataFiltered.stream().findFirst().get();
+  }
+}
diff --git a/pom.xml b/pom.xml
index 94020bc83d3..f635b25b2bb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -105,6 +105,14 @@
         <version>6.57.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-spanner:current} -->
       </dependency>
 
+      <dependency>
+        <groupId>io.opentelemetry</groupId>
+        <artifactId>opentelemetry-bom</artifactId>
+        <version>1.34.1</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-shared-dependencies</artifactId>
diff --git a/samples/snippets/src/main/java/com/example/spanner/TracingSample.java b/samples/snippets/src/main/java/com/example/spanner/TracingSample.java
index 9dcd90d7705..a3d4e5bd072 100644
--- a/samples/snippets/src/main/java/com/example/spanner/TracingSample.java
+++ b/samples/snippets/src/main/java/com/example/spanner/TracingSample.java
@@ -32,7 +32,11 @@
 import io.opencensus.trace.samplers.Samplers;
 import java.util.Arrays;
 
-/** This sample demonstrates how to enable opencensus tracing and stats in cloud spanner client. */
+/** This sample demonstrates how to enable opencensus tracing and stats in cloud spanner client. 
+ *
+ * @deprecated The OpenCensus project is deprecated. Use OpenTelemetry to enable metrics 
+ * and stats with cloud spanner client. 
+*/
 public class TracingSample {
 
   private static final String SAMPLE_SPAN = "CloudSpannerSample";