Repository: GoogleCloudPlatform/pgadapter
Pull request #1465 (2024-02-26 11:59:03)

chore: support OpenTelemetry metrics
Add two latency metrics: 

* `spanner/pgadapter/roundtrip_latencies`: measure the roundtrip latency in PGAdapter that can include multiple messages between the client and PGAdapter. 
* `spanner/pgadapter/client_lib_latencies`: measure the time to make a request and get a response from the client library. 


hengfengli: @olavloite , currently, `client_lib_latencies` only covers `execute_on_spanner` but `execute_batch_on_spanner` is not included. Do we want to include `execute_batch_on_spanner` in `client_lib_latencies` as well? 
olavloite: @hengfengli I took the liberty of doing a small refactor of the code in order to simplify things a bit. I think it is better if we only keep the tracer and metrics in one place (BackendConnection). In addition, we do not need to create a new `Metrics` instance for each connection, as it does not have any connection-specific state. Instead, one instance per `ProxyServer` is enough.
olavloite: > @olavloite , currently, `client_lib_latencies` only covers `execute_on_spanner` but `execute_batch_on_spanner` is not included. Do we want to include `execute_batch_on_spanner` in `client_lib_latencies` as well?

Yes, that should be included as well. `execute_batch_on_spanner` is the same as `ExecuteBatchDml` (and `UpdateDatabaseDdlRequest` with multiple DDL statements, but that is less relevant here).

Patch:
diff --git a/benchmarks/latency-comparison/java/pom.xml b/benchmarks/latency-comparison/java/pom.xml
index 5b26eebe..7dfbd7f4 100644
--- a/benchmarks/latency-comparison/java/pom.xml
+++ b/benchmarks/latency-comparison/java/pom.xml
@@ -21,7 +21,7 @@
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 
-    <junixsocket.version>2.8.3</junixsocket.version>
+    <junixsocket.version>2.9.0</junixsocket.version>
   </properties>
 
   <modelVersion>4.0.0</modelVersion>
@@ -35,7 +35,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-shared-config</artifactId>
-    <version>1.7.1</version>
+    <version>1.7.4</version>
   </parent>
 
   <dependencyManagement>
diff --git a/docs/open_telemetry.md b/docs/open_telemetry.md
index 2fd2bdb1..67cc087c 100644
--- a/docs/open_telemetry.md
+++ b/docs/open_telemetry.md
@@ -1,7 +1,8 @@
-# Open Telemetry in PGAdapter
+# OpenTelemetry in PGAdapter
 
-PGAdapter supports Open Telemetry tracing. You can enable this by adding the `-enable_otel` command
-line argument when starting PGAdapter.
+PGAdapter supports OpenTelemetry tracing and metrics. You can enable the tracing or the metrics
+by adding the `-enable_otel` or `-enable_otel_metrics` command line argument when starting
+PGAdapter. Tracing and metrics can be enabled at the same time.
 
 Optionally, you can also set a trace sample ratio to limit the number of traces that will be
 collected and set with the `-otel_trace_ratio=<ratio>` command line argument. If you omit this
@@ -16,7 +17,8 @@ docker run \
   gcr.io/cloud-spanner-pg-adapter/pgadapter \
   -p my-project -i my-instance \
   -c /credentials.json -x \
-  -enable_otel -otel_trace_ratio=0.1
+  -enable_otel -otel_trace_ratio=0.1 \
+  -enable_otel_metrics
 ```
 
 ## Exporter
@@ -144,6 +146,15 @@ statements consists of the following spans:
 
 ![PGAdapter Cloud Trace - Batch DML example](img/dml_batch_trace_sample.png?raw=true "PGAdapter Cloud Trace - Batch DML example")
 
+## Metrics
+
+The available metrics in PGAdapter are:
+
+* `spanner/pgadapter/roundtrip_latencies`: Latency between PGAdapter receiving a statement from
+   the client and PGAdapter returning the last row of the response to the client.
+* `spanner/pgadapter/client_lib_latencies`: Latency when the Spanner's client library receives
+   a call and returns a response.
+
 ## Frequently Asked Questions
 
 #### How can I find all the statements that were executed on the same connection as the trace I'm looking at?
diff --git a/pom.xml b/pom.xml
index fb6ec224..d2756388 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,6 +37,7 @@
     <excludedIntegrationTests/>
 
     <junixsocket.version>2.9.0</junixsocket.version>
+    <opentelemetry.exporter.version>0.23.0</opentelemetry.exporter.version>
   </properties>
 
   <modelVersion>4.0.0</modelVersion>
@@ -53,7 +54,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-shared-config</artifactId>
-    <version>1.7.1</version>
+    <version>1.7.4</version>
   </parent>
   <scm>
     <connection>scm:git:git@github.com:GoogleCloudPlatform/pgadapter.git</connection>
@@ -141,7 +142,12 @@
     <dependency>
       <groupId>com.google.cloud.opentelemetry</groupId>
       <artifactId>exporter-trace</artifactId>
-      <version>0.23.0</version>
+      <version>${opentelemetry.exporter.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.google.cloud.opentelemetry</groupId>
+      <artifactId>exporter-metrics</artifactId>
+      <version>${opentelemetry.exporter.version}</version>
     </dependency>
     <dependency>
       <groupId>org.jetbrains.kotlin</groupId>
diff --git a/samples/java/cloud-run/pom.xml b/samples/java/cloud-run/pom.xml
index 0b656940..51fdc1bc 100644
--- a/samples/java/cloud-run/pom.xml
+++ b/samples/java/cloud-run/pom.xml
@@ -18,7 +18,7 @@
     <maven.compiler.source>17</maven.compiler.source>
     <maven.compiler.target>17</maven.compiler.target>
     <java.version>17</java.version>
-    <junixsocket.version>2.8.3</junixsocket.version>
+    <junixsocket.version>2.9.0</junixsocket.version>
   </properties>
 
   <modelVersion>4.0.0</modelVersion>
@@ -96,7 +96,7 @@
       <plugin>
         <groupId>com.google.cloud.tools</groupId>
         <artifactId>jib-maven-plugin</artifactId>
-        <version>3.4.0</version>
+        <version>3.4.1</version>
         <configuration>
           <to>
             <image>gcr.io/PROJECT_ID/pgadapter-example</image>
diff --git a/samples/java/jdbc/pom.xml b/samples/java/jdbc/pom.xml
index 238ff63f..e5702a31 100644
--- a/samples/java/jdbc/pom.xml
+++ b/samples/java/jdbc/pom.xml
@@ -21,7 +21,7 @@
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 
-    <junixsocket.version>2.8.3</junixsocket.version>
+    <junixsocket.version>2.9.0</junixsocket.version>
   </properties>
 
   <modelVersion>4.0.0</modelVersion>
diff --git a/samples/java/liquibase/pom.xml b/samples/java/liquibase/pom.xml
index 4716746a..805df2b8 100644
--- a/samples/java/liquibase/pom.xml
+++ b/samples/java/liquibase/pom.xml
@@ -37,7 +37,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-shared-config</artifactId>
-    <version>1.7.1</version>
+    <version>1.7.4</version>
   </parent>
   <scm>
     <connection>scm:git:git@github.com:GoogleCloudPlatform/pgadapter.git</connection>
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java b/src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java
index ed638ff4..9dbf2f1c 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java
@@ -25,9 +25,11 @@ import com.google.cloud.spanner.pgadapter.ConnectionHandler.QueryMode;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata.TextFormat;
 import com.google.cloud.spanner.pgadapter.statements.IntermediateStatement;
+import com.google.cloud.spanner.pgadapter.utils.Metrics;
 import com.google.cloud.spanner.pgadapter.wireprotocol.WireMessage;
 import com.google.common.collect.ImmutableList;
 import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.trace.Tracer;
 import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
@@ -59,6 +61,7 @@ public class ProxyServer extends AbstractApiService {
   private static final Logger logger = Logger.getLogger(ProxyServer.class.getName());
   private final OptionsMetadata options;
   private final OpenTelemetry openTelemetry;
+  private final Metrics metrics;
   private final Properties properties;
   private final List<ConnectionHandler> handlers = Collections.synchronizedList(new LinkedList<>());
 
@@ -117,6 +120,10 @@ public class ProxyServer extends AbstractApiService {
       OptionsMetadata optionsMetadata, OpenTelemetry openTelemetry, Properties properties) {
     this.options = optionsMetadata;
     this.openTelemetry = openTelemetry;
+    this.metrics =
+        optionsMetadata.isEnableOpenTelemetryMetrics()
+            ? new Metrics(openTelemetry)
+            : new Metrics(OpenTelemetry.noop());
     this.localPort = optionsMetadata.getProxyPort();
     this.properties = properties;
     this.debugMode = optionsMetadata.isDebugMode();
@@ -372,6 +379,16 @@ public class ProxyServer extends AbstractApiService {
     return this.openTelemetry;
   }
 
+  public Tracer getTracer(String name, String version) {
+    return getOptions().isEnableOpenTelemetry()
+        ? getOpenTelemetry().getTracer(name, version)
+        : OpenTelemetry.noop().getTracer(name, version);
+  }
+
+  public Metrics getMetrics() {
+    return this.metrics;
+  }
+
   /** @return the JDBC connection properties that are used by this server */
   public Properties getProperties() {
     return (Properties) this.properties.clone();
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/Server.java b/src/main/java/com/google/cloud/spanner/pgadapter/Server.java
index ddc245f2..56b5d37c 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/Server.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/Server.java
@@ -15,6 +15,8 @@
 package com.google.cloud.spanner.pgadapter;
 
 import com.google.auth.Credentials;
+import com.google.cloud.opentelemetry.metric.GoogleCloudMetricExporter;
+import com.google.cloud.opentelemetry.metric.MetricConfiguration;
 import com.google.cloud.opentelemetry.trace.TraceConfiguration;
 import com.google.cloud.opentelemetry.trace.TraceExporter;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
@@ -23,6 +25,9 @@ import com.google.devtools.cloudtrace.v2.AttributeValue;
 import com.google.devtools.cloudtrace.v2.TruncatableString;
 import io.opentelemetry.api.OpenTelemetry;
 import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
+import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder;
+import io.opentelemetry.sdk.metrics.export.MetricExporter;
+import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
 import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
 import io.opentelemetry.sdk.trace.export.SpanExporter;
 import io.opentelemetry.sdk.trace.samplers.Sampler;
@@ -56,7 +61,8 @@ public class Server {
 
   /** Creates an {@link OpenTelemetry} object from the given options. */
   static OpenTelemetry setupOpenTelemetry(OptionsMetadata optionsMetadata) {
-    if (!optionsMetadata.isEnableOpenTelemetry()) {
+    if (!optionsMetadata.isEnableOpenTelemetry()
+        && !optionsMetadata.isEnableOpenTelemetryMetrics()) {
       return OpenTelemetry.noop();
     }
 
@@ -73,45 +79,62 @@ public class Server {
       System.setProperty("otel.service.name", "pgadapter");
     }
 
-    TraceConfiguration.Builder builder =
-        TraceConfiguration.builder().setDeadline(Duration.ofSeconds(60L));
-    String projectId = optionsMetadata.getTelemetryProjectId();
-    if (projectId != null) {
-      builder.setProjectId(projectId);
-    }
     try {
+      String projectId = optionsMetadata.getTelemetryProjectId();
       Credentials credentials = optionsMetadata.getTelemetryCredentials();
-      if (credentials != null) {
-        builder.setCredentials(credentials);
+      AutoConfiguredOpenTelemetrySdkBuilder openTelemetryBuilder =
+          AutoConfiguredOpenTelemetrySdk.builder();
+      if (optionsMetadata.isEnableOpenTelemetry()) {
+        TraceConfiguration.Builder builder =
+            TraceConfiguration.builder().setDeadline(Duration.ofSeconds(60L));
+        if (projectId != null) {
+          builder.setProjectId(projectId);
+        }
+        if (credentials != null) {
+          builder.setCredentials(credentials);
+        }
+        builder.setFixedAttributes(
+            ImmutableMap.of(
+                "service.name",
+                AttributeValue.newBuilder()
+                    .setStringValue(
+                        TruncatableString.newBuilder()
+                            .setValue(
+                                Objects.requireNonNull(
+                                    getOpenTelemetrySetting("otel.service.name")))
+                            .build())
+                    .build()));
+        TraceConfiguration configuration = builder.build();
+        SpanExporter traceExporter = TraceExporter.createWithConfiguration(configuration);
+        Sampler sampler;
+        if (optionsMetadata.getOpenTelemetryTraceRatio() == null) {
+          sampler = Sampler.parentBased(Sampler.traceIdRatioBased(0.05d));
+        } else {
+          sampler =
+              Sampler.parentBased(
+                  Sampler.traceIdRatioBased(optionsMetadata.getOpenTelemetryTraceRatio()));
+        }
+        openTelemetryBuilder.addTracerProviderCustomizer(
+            (sdkTracerProviderBuilder, configProperties) ->
+                sdkTracerProviderBuilder
+                    .setSampler(sampler)
+                    .addSpanProcessor(BatchSpanProcessor.builder(traceExporter).build()));
       }
-      builder.setFixedAttributes(
-          ImmutableMap.of(
-              "service.name",
-              AttributeValue.newBuilder()
-                  .setStringValue(
-                      TruncatableString.newBuilder()
-                          .setValue(
-                              Objects.requireNonNull(getOpenTelemetrySetting("otel.service.name")))
-                          .build())
-                  .build()));
-      TraceConfiguration configuration = builder.build();
-      SpanExporter traceExporter = TraceExporter.createWithConfiguration(configuration);
-      Sampler sampler;
-      if (optionsMetadata.getOpenTelemetryTraceRatio() == null) {
-        sampler = Sampler.parentBased(Sampler.traceIdRatioBased(0.05d));
-      } else {
-        sampler =
-            Sampler.parentBased(
-                Sampler.traceIdRatioBased(optionsMetadata.getOpenTelemetryTraceRatio()));
+      if (optionsMetadata.isEnableOpenTelemetryMetrics()) {
+        MetricExporter cloudMonitoringExporter =
+            GoogleCloudMetricExporter.createWithConfiguration(
+                MetricConfiguration.builder()
+                    // Configure the cloud project id.
+                    .setProjectId(projectId)
+                    // Set the credentials to use when writing to the Cloud Monitoring API
+                    .setCredentials(credentials)
+                    .build());
+        openTelemetryBuilder.addMeterProviderCustomizer(
+            (sdkMeterProviderBuilder, configProperties) ->
+                sdkMeterProviderBuilder.registerMetricReader(
+                    PeriodicMetricReader.builder(cloudMonitoringExporter).build()));
       }
-      return AutoConfiguredOpenTelemetrySdk.builder()
-          .addTracerProviderCustomizer(
-              (sdkTracerProviderBuilder, configProperties) ->
-                  sdkTracerProviderBuilder
-                      .setSampler(sampler)
-                      .addSpanProcessor(BatchSpanProcessor.builder(traceExporter).build()))
-          .build()
-          .getOpenTelemetrySdk();
+      return openTelemetryBuilder.build().getOpenTelemetrySdk();
     } catch (IOException exception) {
       throw new RuntimeException(exception);
     }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadata.java b/src/main/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadata.java
index 0bcdb68b..abc168de 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadata.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/metadata/OptionsMetadata.java
@@ -550,6 +550,7 @@ public class OptionsMetadata {
   private static final String OPTION_BINARY_FORMAT = "b";
   private static final String OPTION_AUTHENTICATE = "a";
   private static final String OPTION_ENABLE_OPEN_TELEMETRY = "enable_otel";
+  private static final String OPTION_ENABLE_OPEN_TELEMETRY_METRICS = "enable_otel_metrics";
   private static final String OPTION_OPEN_TELEMETRY_TRACE_RATIO = "otel_trace_ratio";
   private static final String OPTION_SSL = "ssl";
   private static final String OPTION_DISABLE_AUTO_DETECT_CLIENT = "disable_auto_detect_client";
@@ -593,6 +594,7 @@ public class OptionsMetadata {
   private final boolean binaryFormat;
   private final boolean authenticate;
   private final boolean enableOpenTelemetry;
+  private final boolean enableOpenTelemetryMetrics;
   private final Double openTelemetryTraceRatio;
   private final SslMode sslMode;
   private final boolean disableAutoDetectClient;
@@ -679,6 +681,7 @@ public class OptionsMetadata {
     this.binaryFormat = commandLine.hasOption(OPTION_BINARY_FORMAT);
     this.authenticate = commandLine.hasOption(OPTION_AUTHENTICATE);
     this.enableOpenTelemetry = commandLine.hasOption(OPTION_ENABLE_OPEN_TELEMETRY);
+    this.enableOpenTelemetryMetrics = commandLine.hasOption(OPTION_ENABLE_OPEN_TELEMETRY_METRICS);
     this.openTelemetryTraceRatio =
         parseOpenTelemetryTraceRatio(commandLine.getOptionValue(OPTION_OPEN_TELEMETRY_TRACE_RATIO));
     this.sslMode = parseSslMode(commandLine.getOptionValue(OPTION_SSL));
@@ -757,6 +760,7 @@ public class OptionsMetadata {
     this.binaryFormat = forceBinary;
     this.authenticate = authenticate;
     this.enableOpenTelemetry = false;
+    this.enableOpenTelemetryMetrics = false;
     this.openTelemetryTraceRatio = null;
     this.sslMode = SslMode.Disable;
     this.disableAutoDetectClient = false;
@@ -1124,6 +1128,8 @@ public class OptionsMetadata {
         false,
         "Whether you wish the proxy to perform an authentication step.");
     options.addOption(null, OPTION_ENABLE_OPEN_TELEMETRY, false, "Enable OpenTelemetry tracing.");
+    options.addOption(
+        null, OPTION_ENABLE_OPEN_TELEMETRY_METRICS, false, "Enable OpenTelemetry metrics.");
     options.addOption(
         null,
         OPTION_OPEN_TELEMETRY_TRACE_RATIO,
@@ -1464,6 +1470,10 @@ public class OptionsMetadata {
     return this.enableOpenTelemetry;
   }
 
+  public boolean isEnableOpenTelemetryMetrics() {
+    return this.enableOpenTelemetryMetrics;
+  }
+
   public Double getOpenTelemetryTraceRatio() {
     return this.openTelemetryTraceRatio;
   }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java
index 2a639e5c..f9f091b9 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java
@@ -64,6 +64,7 @@ import com.google.cloud.spanner.pgadapter.utils.ClientAutoDetector.WellKnownClie
 import com.google.cloud.spanner.pgadapter.utils.CopyDataReceiver;
 import com.google.cloud.spanner.pgadapter.utils.Logging;
 import com.google.cloud.spanner.pgadapter.utils.Logging.Action;
+import com.google.cloud.spanner.pgadapter.utils.Metrics;
 import com.google.cloud.spanner.pgadapter.utils.MutationWriter;
 import com.google.cloud.spanner.pgadapter.wireoutput.ReadyResponse;
 import com.google.cloud.spanner.pgadapter.wireoutput.ReadyResponse.Status;
@@ -80,6 +81,7 @@ import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.SettableFuture;
+import io.opentelemetry.api.common.Attributes;
 import io.opentelemetry.api.trace.Span;
 import io.opentelemetry.api.trace.SpanBuilder;
 import io.opentelemetry.api.trace.StatusCode;
@@ -123,6 +125,10 @@ public class BackendConnection {
 
   private final Tracer tracer;
 
+  private final Metrics metrics;
+
+  private final Attributes metricAttributes;
+
   private final Deque<Context> statementContext = new ConcurrentLinkedDeque<>();
 
   private final String connectionId;
@@ -472,6 +478,7 @@ public class BackendConnection {
         Stopwatch stopwatch = Stopwatch.createStarted();
         StatementResult result = spannerConnection.execute(statement);
         Duration executionDuration = stopwatch.elapsed();
+        metrics.recordClientLibLatency(executionDuration.toMillis(), metricAttributes);
         logger.log(
             Level.FINER,
             Logging.format(
@@ -629,9 +636,7 @@ public class BackendConnection {
 
         // Make sure both the front-end CopyDataReceiver and the backend MutationWriter processes
         // have finished before we proceed.
-        //noinspection UnstableApiUsage
         Futures.successfulAsList(copyDataReceiverFuture, statementResultFuture).get();
-        //noinspection UnstableApiUsage
         Futures.allAsList(copyDataReceiverFuture, statementResultFuture).get();
       } catch (ExecutionException executionException) {
         result.setException(executionException.getCause());
@@ -838,6 +843,8 @@ public class BackendConnection {
   /** Creates a PG backend connection that uses the given Spanner {@link Connection} and options. */
   BackendConnection(
       Tracer tracer,
+      Metrics metrics,
+      Attributes metricAttributes,
       String connectionId,
       Runnable closeAllPortals,
       DatabaseId databaseId,
@@ -846,6 +853,8 @@ public class BackendConnection {
       OptionsMetadata optionsMetadata,
       Supplier<ImmutableList<LocalStatement>> localStatements) {
     this.tracer = tracer;
+    this.metrics = metrics;
+    this.metricAttributes = metricAttributes;
     this.connectionId = connectionId;
     this.closeAllPortals = closeAllPortals;
     this.sessionState = new SessionState(optionsMetadata);
@@ -914,6 +923,20 @@ public class BackendConnection {
     return this.connectionState;
   }
 
+  @VisibleForTesting
+  public Tracer getTracer() {
+    return this.tracer;
+  }
+
+  @VisibleForTesting
+  public Metrics getMetrics() {
+    return this.metrics;
+  }
+
+  Attributes getMetricAttributes() {
+    return metricAttributes;
+  }
+
   private Span createSpan(String name, Statement statement) {
     return createSpan(name, statement, null);
   }
@@ -1474,7 +1497,10 @@ public class BackendConnection {
       }
       Span runBatchSpan = createSpan("execute_batch_on_spanner", null);
       try (Scope ignoreRunBatchSpan = runBatchSpan.makeCurrent()) {
+        Stopwatch stopwatch = Stopwatch.createStarted();
         long[] counts = spannerConnection.runBatch();
+        Duration executionDuration = stopwatch.elapsed();
+        metrics.recordClientLibLatency(executionDuration.toMillis(), metricAttributes);
         if (batchType == StatementType.DDL) {
           counts = extractDdlUpdateCounts(statementResults, counts);
         }
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/ExtendedQueryProtocolHandler.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/ExtendedQueryProtocolHandler.java
index cdc165c7..29a4bda9 100644
--- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/ExtendedQueryProtocolHandler.java
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/ExtendedQueryProtocolHandler.java
@@ -16,6 +16,7 @@ package com.google.cloud.spanner.pgadapter.statements;
 
 import static com.google.cloud.spanner.pgadapter.Server.getVersion;
 
+import com.google.cloud.spanner.DatabaseId;
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
 import com.google.cloud.spanner.pgadapter.error.PGExceptionFactory;
 import com.google.cloud.spanner.pgadapter.utils.Logging;
@@ -25,7 +26,9 @@ import com.google.cloud.spanner.pgadapter.wireprotocol.AbstractQueryProtocolMess
 import com.google.cloud.spanner.pgadapter.wireprotocol.SyncMessage;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
 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 io.opentelemetry.api.trace.Tracer;
@@ -50,29 +53,27 @@ public class ExtendedQueryProtocolHandler {
   private final BackendConnection backendConnection;
 
   private final String connectionId;
-  private final Tracer tracer;
   private volatile Span span;
   private volatile Scope scope;
+  private volatile Stopwatch stopwatch;
 
   /** Creates an {@link ExtendedQueryProtocolHandler} for the given connection. */
   public ExtendedQueryProtocolHandler(ConnectionHandler connectionHandler) {
-    this.connectionHandler = Preconditions.checkNotNull(connectionHandler);
-    this.connectionId = connectionHandler.getTraceConnectionId().toString();
-    this.tracer =
-        connectionHandler
-            .getServer()
-            .getOpenTelemetry()
-            .getTracer(ExtendedQueryProtocolHandler.class.getName(), getVersion());
-    this.backendConnection =
+    this(
+        connectionHandler,
         new BackendConnection(
-            tracer,
+            connectionHandler
+                .getServer()
+                .getTracer(ConnectionHandler.class.getName(), getVersion()),
+            connectionHandler.getServer().getMetrics(),
+            createMetricAttributes(connectionHandler.getDatabaseId()),
             connectionHandler.getTraceConnectionId().toString(),
             connectionHandler::closeAllPortals,
             connectionHandler.getDatabaseId(),
             connectionHandler.getSpannerConnection(),
             connectionHandler::getWellKnownClient,
             connectionHandler.getServer().getOptions(),
-            () -> connectionHandler.getWellKnownClient().getLocalStatements(connectionHandler));
+            () -> connectionHandler.getWellKnownClient().getLocalStatements(connectionHandler)));
   }
 
   /** Constructor only intended for testing. */
@@ -81,23 +82,27 @@ public class ExtendedQueryProtocolHandler {
       ConnectionHandler connectionHandler, BackendConnection backendConnection) {
     this.connectionHandler = Preconditions.checkNotNull(connectionHandler);
     this.connectionId = connectionHandler.getTraceConnectionId().toString();
-    this.tracer =
-        connectionHandler
-            .getServer()
-            .getOpenTelemetry()
-            .getTracer(ExtendedQueryProtocolHandler.class.getName(), getVersion());
     this.backendConnection = Preconditions.checkNotNull(backendConnection);
   }
 
-  public Tracer getTracer() {
-    return tracer;
-  }
-
   /** Returns the backend PG connection for this query handler. */
   public BackendConnection getBackendConnection() {
     return backendConnection;
   }
 
+  public Tracer getTracer() {
+    return backendConnection.getTracer();
+  }
+
+  @VisibleForTesting
+  static Attributes createMetricAttributes(DatabaseId databaseId) {
+    AttributesBuilder attributesBuilder = Attributes.builder();
+    attributesBuilder.put("database", databaseId.getDatabase());
+    attributesBuilder.put("instance_id", databaseId.getInstanceId().getInstance());
+    attributesBuilder.put("project_id", databaseId.getInstanceId().getProject());
+    return attributesBuilder.build();
+  }
+
   /** Returns a copy of the currently buffered messages in this handler. */
   @VisibleForTesting
   List<AbstractQueryProtocolMessage> getMessages() {
@@ -115,8 +120,10 @@ public class ExtendedQueryProtocolHandler {
 
   public void maybeStartSpan(boolean isExtendedProtocol) {
     if (span == null) {
+      stopwatch = Stopwatch.createStarted();
       span =
-          tracer
+          getBackendConnection()
+              .getTracer()
               .spanBuilder("query_protocol_handler")
               .setNoParent()
               .setAttribute("pgadapter.query_protocol", isExtendedProtocol ? "extended" : "simple")
@@ -244,6 +251,10 @@ public class ExtendedQueryProtocolHandler {
       scope.close();
       span.end();
       span = null;
+      backendConnection
+          .getMetrics()
+          .recordPGAdapterLatency(
+              stopwatch.elapsed().toMillis(), backendConnection.getMetricAttributes());
     }
   }
 
diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/utils/Metrics.java b/src/main/java/com/google/cloud/spanner/pgadapter/utils/Metrics.java
new file mode 100644
index 00000000..30392963
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/pgadapter/utils/Metrics.java
@@ -0,0 +1,71 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.cloud.spanner.pgadapter.utils;
+
+import com.google.api.core.InternalApi;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.util.Arrays;
+import java.util.List;
+
+@InternalApi
+public class Metrics {
+  static final String INSTRUMENTATION_SCOPE = "cloud.google.com/java";
+  static final String SPANNER_CLIENT_LIB_LATENCY = "spanner/pgadapter/client_lib_latencies";
+  static final String SPANNER_CLIENT_LIB_LATENCY_DESCRIPTION =
+      "Latency when the client library receives a call and returns a response";
+  static final String PGADAPTER_LATENCY = "spanner/pgadapter/roundtrip_latencies";
+  static final String PGADAPTER_LATENCY_DESCRIPTION =
+      "Latency between PGAdapter receiving a statement from the client and PGAdapter returning the last row of the response to the client";
+
+  private final LongHistogram spannerClientLibLatencies;
+  private final LongHistogram pgadapterLatencies;
+
+  public Metrics(OpenTelemetry openTelemetry) {
+    Meter meter = openTelemetry.getMeter(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);
+    spannerClientLibLatencies =
+        meter
+            .histogramBuilder(SPANNER_CLIENT_LIB_LATENCY)
+            .ofLongs()
+            .setDescription(SPANNER_CLIENT_LIB_LATENCY_DESCRIPTION)
+            .setUnit("ms")
+            .setExplicitBucketBoundariesAdvice(RPC_MILLIS_BUCKET_BOUNDARIES)
+            .build();
+    pgadapterLatencies =
+        meter
+            .histogramBuilder(PGADAPTER_LATENCY)
+            .ofLongs()
+            .setDescription(PGADAPTER_LATENCY_DESCRIPTION)
+            .setUnit("ms")
+            .setExplicitBucketBoundariesAdvice(RPC_MILLIS_BUCKET_BOUNDARIES)
+            .build();
+  }
+
+  @InternalApi
+  public void recordClientLibLatency(long value, Attributes attributes) {
+    spannerClientLibLatencies.record(value, attributes);
+  }
+
+  @InternalApi
+  public void recordPGAdapterLatency(long value, Attributes attributes) {
+    pgadapterLatencies.record(value, attributes);
+  }
+}


Test patch:
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/CopyInMockServerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/CopyInMockServerTest.java
index 64051ace..07cf79b5 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/CopyInMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/CopyInMockServerTest.java
@@ -87,6 +87,7 @@ import org.postgresql.core.PGStream;
 import org.postgresql.core.QueryExecutorBase;
 import org.postgresql.core.v3.QueryExecutorImpl;
 import org.postgresql.jdbc.PgConnection;
+import org.postgresql.util.PSQLException;
 
 @RunWith(Parameterized.class)
 public class CopyInMockServerTest extends AbstractMockServerTest {
@@ -937,6 +938,12 @@ public class CopyInMockServerTest extends AbstractMockServerTest {
       stream.sendChar('x');
       stream.sendInteger4(4);
       stream.flush();
+    } catch (PSQLException exception) {
+      // Ignore any exceptions about not being able to close the connection properly.
+      // These happen from time to time as we are deliberately messing with the connection.
+      if (!exception.getMessage().contains("Unable to close connection properly")) {
+        throw exception;
+      }
     }
 
     List<CommitRequest> commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class);
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/BackendConnectionTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/BackendConnectionTest.java
index eb5aea2b..d1809527 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/statements/BackendConnectionTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/BackendConnectionTest.java
@@ -59,9 +59,11 @@ import com.google.cloud.spanner.pgadapter.statements.local.ListDatabasesStatemen
 import com.google.cloud.spanner.pgadapter.statements.local.LocalStatement;
 import com.google.cloud.spanner.pgadapter.utils.ClientAutoDetector.WellKnownClient;
 import com.google.cloud.spanner.pgadapter.utils.CopyDataReceiver;
+import com.google.cloud.spanner.pgadapter.utils.Metrics;
 import com.google.cloud.spanner.pgadapter.utils.MutationWriter;
 import com.google.common.collect.ImmutableList;
 import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.Attributes;
 import io.opentelemetry.api.trace.Tracer;
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
@@ -76,10 +78,14 @@ import org.junit.runners.JUnit4;
 @RunWith(JUnit4.class)
 public class BackendConnectionTest {
   private static final Tracer NOOP_OTEL = OpenTelemetry.noop().getTracer("test");
+  private static final Metrics NOOP_OTEL_METER = new Metrics(OpenTelemetry.noop());
   private final AbstractStatementParser PARSER =
       AbstractStatementParser.getInstance(Dialect.POSTGRESQL);
   private static final NoResult NO_RESULT = new NoResult();
   private static final Runnable DO_NOTHING = () -> {};
+  private static final DatabaseId DATABASE_ID = DatabaseId.of("p", "i", "d");
+  private static final Attributes METRIC_ATTRIBUTES =
+      ExtendedQueryProtocolHandler.createMetricAttributes(DATABASE_ID);
 
   @Test
   public void testExtractDdlUpdateCounts() {
@@ -117,9 +123,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             spannerConnection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -157,9 +165,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             spannerConnection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -198,9 +208,11 @@ public class BackendConnectionTest {
     BackendConnection onlyDmlStatements =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             spannerConnection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -215,9 +227,11 @@ public class BackendConnectionTest {
     BackendConnection onlyCopyStatements =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             spannerConnection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -230,9 +244,11 @@ public class BackendConnectionTest {
     BackendConnection dmlAndCopyStatements =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             spannerConnection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -247,9 +263,11 @@ public class BackendConnectionTest {
     BackendConnection onlySelectStatements =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             spannerConnection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -264,9 +282,11 @@ public class BackendConnectionTest {
     BackendConnection onlyClientSideStatements =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             spannerConnection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -281,9 +301,11 @@ public class BackendConnectionTest {
     BackendConnection onlyUnknownStatements =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             spannerConnection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -298,9 +320,11 @@ public class BackendConnectionTest {
     BackendConnection dmlAndSelectStatements =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             spannerConnection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -315,9 +339,11 @@ public class BackendConnectionTest {
     BackendConnection copyAndSelectStatements =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             spannerConnection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -332,9 +358,11 @@ public class BackendConnectionTest {
     BackendConnection copyAndUnknownStatements =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             spannerConnection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -365,9 +393,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -405,9 +435,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -444,9 +476,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -473,9 +507,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -514,9 +550,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -545,9 +583,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             options,
@@ -574,9 +614,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             options,
@@ -604,9 +646,11 @@ public class BackendConnectionTest {
       BackendConnection backendConnection =
           new BackendConnection(
               NOOP_OTEL,
+              NOOP_OTEL_METER,
+              METRIC_ATTRIBUTES,
               UUID.randomUUID().toString(),
               DO_NOTHING,
-              DatabaseId.of("p", "i", "d"),
+              DATABASE_ID,
               connection,
               () -> WellKnownClient.UNSPECIFIED,
               mock(OptionsMetadata.class),
@@ -630,9 +674,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -662,9 +708,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -692,9 +740,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -721,9 +771,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -760,9 +812,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
@@ -798,9 +852,11 @@ public class BackendConnectionTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            DATABASE_ID,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             mock(OptionsMetadata.class),
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/ExtendedQueryProtocolHandlerTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/ExtendedQueryProtocolHandlerTest.java
index 5c3ff9de..573c0afd 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/statements/ExtendedQueryProtocolHandlerTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/ExtendedQueryProtocolHandlerTest.java
@@ -22,7 +22,6 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import com.google.cloud.spanner.pgadapter.ConnectionHandler;
-import com.google.cloud.spanner.pgadapter.ProxyServer;
 import com.google.cloud.spanner.pgadapter.error.PGException;
 import com.google.cloud.spanner.pgadapter.error.SQLState;
 import com.google.cloud.spanner.pgadapter.metadata.ConnectionMetadata;
@@ -31,7 +30,6 @@ import com.google.cloud.spanner.pgadapter.wireprotocol.DescribeMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ExecuteMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ParseMessage;
 import com.google.common.collect.ImmutableList;
-import io.opentelemetry.api.OpenTelemetry;
 import java.io.DataOutputStream;
 import java.util.UUID;
 import org.junit.Before;
@@ -46,16 +44,12 @@ import org.mockito.junit.MockitoRule;
 @RunWith(JUnit4.class)
 public class ExtendedQueryProtocolHandlerTest {
   @Rule public MockitoRule rule = MockitoJUnit.rule();
-
-  @Mock private ProxyServer server;
   @Mock private ConnectionHandler connectionHandler;
   @Mock private BackendConnection backendConnection;
 
   @Before
   public void setupMocks() {
-    when(connectionHandler.getServer()).thenReturn(server);
     when(connectionHandler.getTraceConnectionId()).thenReturn(UUID.randomUUID());
-    when(server.getOpenTelemetry()).thenReturn(OpenTelemetry.noop());
   }
 
   @Test
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/statements/StatementTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/statements/StatementTest.java
index f285eaa2..2eb88acd 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/statements/StatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/statements/StatementTest.java
@@ -54,6 +54,7 @@ import com.google.cloud.spanner.pgadapter.metadata.ConnectionMetadata;
 import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
 import com.google.cloud.spanner.pgadapter.session.SessionState;
 import com.google.cloud.spanner.pgadapter.utils.ClientAutoDetector.WellKnownClient;
+import com.google.cloud.spanner.pgadapter.utils.Metrics;
 import com.google.cloud.spanner.pgadapter.utils.MutationWriter;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage;
 import com.google.cloud.spanner.pgadapter.wireprotocol.QueryMessage;
@@ -61,6 +62,7 @@ import com.google.cloud.spanner.pgadapter.wireprotocol.WireMessage;
 import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Bytes;
 import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.Attributes;
 import io.opentelemetry.api.trace.Tracer;
 import java.io.ByteArrayInputStream;
 import java.io.DataInputStream;
@@ -90,8 +92,12 @@ public class StatementTest {
       AbstractStatementParser.getInstance(Dialect.POSTGRESQL);
 
   private static final Tracer NOOP_OTEL = OpenTelemetry.noop().getTracer("test");
+  private static final Metrics NOOP_OTEL_METER = new Metrics(OpenTelemetry.noop());
 
   private static final Runnable DO_NOTHING = () -> {};
+  private static final DatabaseId DATABASE_ID = DatabaseId.of("p", "i", "d");
+  private static final Attributes METRIC_ATTRIBUTES =
+      ExtendedQueryProtocolHandler.createMetricAttributes(DATABASE_ID);
 
   private static ParsedStatement parse(String sql) {
     return PARSER.parse(Statement.of(sql));
@@ -189,6 +195,8 @@ public class StatementTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
             connectionHandler.getDatabaseId(),
@@ -282,6 +290,8 @@ public class StatementTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
             connectionHandler.getDatabaseId(),
@@ -324,6 +334,8 @@ public class StatementTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
             connectionHandler.getDatabaseId(),
@@ -434,6 +446,8 @@ public class StatementTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
             connectionHandler.getDatabaseId(),
@@ -502,12 +516,15 @@ public class StatementTest {
             CopyStatement.create(
                 connectionHandler, mock(OptionsMetadata.class), "", parse(sql), Statement.of(sql));
 
+    DatabaseId databaseId = DatabaseId.of("p", "i", "d");
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            ExtendedQueryProtocolHandler.createMetricAttributes(databaseId),
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            databaseId,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             options,
@@ -580,6 +597,8 @@ public class StatementTest {
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            METRIC_ATTRIBUTES,
             UUID.randomUUID().toString(),
             DO_NOTHING,
             connectionHandler.getDatabaseId(),
@@ -605,12 +624,15 @@ public class StatementTest {
     when(backendConnection.getSessionState()).thenReturn(sessionState);
     when(connectionHandler.getConnectionMetadata()).thenReturn(connectionMetadata);
     setupQueryInformationSchemaResults();
+    DatabaseId databaseId = DatabaseId.of("p", "i", "d");
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            ExtendedQueryProtocolHandler.createMetricAttributes(databaseId),
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            databaseId,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             options,
@@ -661,12 +683,15 @@ public class StatementTest {
     SessionState sessionState = new SessionState(options);
     when(backendConnection.getSessionState()).thenReturn(sessionState);
     setupQueryInformationSchemaResults();
+    DatabaseId databaseId = DatabaseId.of("p", "i", "d");
     BackendConnection backendConnection =
         new BackendConnection(
             NOOP_OTEL,
+            NOOP_OTEL_METER,
+            ExtendedQueryProtocolHandler.createMetricAttributes(databaseId),
             UUID.randomUUID().toString(),
             DO_NOTHING,
-            DatabaseId.of("p", "i", "d"),
+            databaseId,
             connection,
             () -> WellKnownClient.UNSPECIFIED,
             options,
diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ProtocolTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ProtocolTest.java
index 50ac9ec2..7a7f4505 100644
--- a/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ProtocolTest.java
+++ b/src/test/java/com/google/cloud/spanner/pgadapter/wireprotocol/ProtocolTest.java
@@ -32,10 +32,7 @@ import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import com.google.cloud.spanner.DatabaseClient;
 import com.google.cloud.spanner.Dialect;
-import com.google.cloud.spanner.ReadContext;
-import com.google.cloud.spanner.ResultSet;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.connection.AbstractStatementParser;
 import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
@@ -61,6 +58,7 @@ import com.google.cloud.spanner.pgadapter.statements.ExtendedQueryProtocolHandle
 import com.google.cloud.spanner.pgadapter.statements.IntermediatePortalStatement;
 import com.google.cloud.spanner.pgadapter.statements.IntermediatePreparedStatement;
 import com.google.cloud.spanner.pgadapter.utils.ClientAutoDetector.WellKnownClient;
+import com.google.cloud.spanner.pgadapter.utils.Metrics;
 import com.google.cloud.spanner.pgadapter.utils.MutationWriter;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage.ManuallyCreatedToken;
 import com.google.cloud.spanner.pgadapter.wireprotocol.ControlMessage.PreparedType;
@@ -81,13 +79,10 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
-import org.json.simple.parser.JSONParser;
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -109,10 +104,6 @@ public class ProtocolTest {
   @Mock private IntermediatePortalStatement intermediatePortalStatement;
   @Mock private ConnectionMetadata connectionMetadata;
   @Mock private DataOutputStream outputStream;
-  @Mock private ResultSet resultSet;
-
-  @Before
-  public void setupMocks() {}
 
   private byte[] intToBytes(int value) {
     byte[] parameters = new byte[4];
@@ -179,7 +170,6 @@ public class ProtocolTest {
 
   @Test
   public void testQueryUsesPSQLStatementWhenPSQLModeSelectedMessage() throws Exception {
-    JSONParser parser = new JSONParser();
     byte[] messageMetadata = {'Q', 0, 0, 0, 24};
     String payload = "SELECT * FROM users\0";
     byte[] value = Bytes.concat(messageMetadata, payload.getBytes());
@@ -1112,9 +1102,7 @@ public class ProtocolTest {
     ByteArrayOutputStream result = new ByteArrayOutputStream();
     DataOutputStream outputStream = new DataOutputStream(result);
 
-    when(connectionHandler.getServer()).thenReturn(server);
     when(connectionHandler.getTraceConnectionId()).thenReturn(UUID.randomUUID());
-    when(server.getOpenTelemetry()).thenReturn(OpenTelemetry.noop());
     ExtendedQueryProtocolHandler extendedQueryProtocolHandler =
         new ExtendedQueryProtocolHandler(connectionHandler, backendConnection);
     when(connectionHandler.getStatus()).thenReturn(ConnectionStatus.AUTHENTICATED);
@@ -1150,9 +1138,7 @@ public class ProtocolTest {
     ByteArrayOutputStream result = new ByteArrayOutputStream();
     DataOutputStream outputStream = new DataOutputStream(result);
 
-    when(connectionHandler.getServer()).thenReturn(server);
     when(connectionHandler.getTraceConnectionId()).thenReturn(UUID.randomUUID());
-    when(server.getOpenTelemetry()).thenReturn(OpenTelemetry.noop());
     ExtendedQueryProtocolHandler extendedQueryProtocolHandler =
         new ExtendedQueryProtocolHandler(connectionHandler, backendConnection);
     when(connectionHandler.getStatus()).thenReturn(ConnectionStatus.AUTHENTICATED);
@@ -1245,7 +1231,6 @@ public class ProtocolTest {
     when(connectionHandler.getConnectionMetadata()).thenReturn(connectionMetadata);
     when(connectionHandler.getServer()).thenReturn(server);
     when(connectionHandler.getTraceConnectionId()).thenReturn(UUID.randomUUID());
-    when(server.getOpenTelemetry()).thenReturn(OpenTelemetry.noop());
     when(connectionHandler.getStatement("")).thenReturn(intermediatePortalStatement);
     when(connectionHandler.getPortal("")).thenReturn(intermediatePortalStatement);
     when(intermediatePortalStatement.getCommandTag()).thenReturn("INSERT");
@@ -1253,6 +1238,8 @@ public class ProtocolTest {
     when(intermediatePortalStatement.getStatementResult()).thenReturn(new UpdateCount(1L));
     when(intermediatePortalStatement.getUpdateCount()).thenReturn(1L);
     when(backendConnection.getConnectionState()).thenReturn(ConnectionState.TRANSACTION);
+    when(backendConnection.getTracer()).thenReturn(OpenTelemetry.noop().getTracer("mock-tracer"));
+    when(backendConnection.getMetrics()).thenReturn(new Metrics(OpenTelemetry.noop()));
     OptionsMetadata options = mock(OptionsMetadata.class);
     when(server.getOptions()).thenReturn(options);
     when(connectionMetadata.getInputStream()).thenReturn(inputStream);
@@ -1938,30 +1925,4 @@ public class ProtocolTest {
     byte[] resultBytes = result.toByteArray();
     assertEquals('E', resultBytes[0]);
   }
-
-  private void setupQueryInformationSchemaResults() {
-    DatabaseClient databaseClient = mock(DatabaseClient.class);
-    ReadContext singleUseReadContext = mock(ReadContext.class);
-    when(databaseClient.singleUse()).thenReturn(singleUseReadContext);
-    when(connectionHandler.getSpannerConnection()).thenReturn(connection);
-    when(connection.getDatabaseClient()).thenReturn(databaseClient);
-    ResultSet spannerType = mock(ResultSet.class);
-    when(spannerType.getString("column_name")).thenReturn("key", "value");
-    when(spannerType.getString("data_type")).thenReturn("bigint", "character varying");
-    when(spannerType.next()).thenReturn(true, true, false);
-    when(singleUseReadContext.executeQuery(
-            ArgumentMatchers.argThat(
-                statement ->
-                    statement != null && statement.getSql().startsWith("SELECT column_name"))))
-        .thenReturn(spannerType);
-
-    ResultSet countResult = mock(ResultSet.class);
-    when(countResult.getLong(0)).thenReturn(2L);
-    when(countResult.next()).thenReturn(true, false);
-    when(singleUseReadContext.executeQuery(
-            ArgumentMatchers.argThat(
-                statement ->
-                    statement != null && statement.getSql().startsWith("SELECT COUNT(*)"))))
-        .thenReturn(countResult);
-  }
 }