Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add OpenTelemetry tracing #1182

Merged
merged 8 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ See [Frequently Asked Questions](docs/faq.md) for answers to frequently asked qu
See [Latency Comparisons](benchmarks/latency-comparison/README.md) for benchmark comparisons between
using PostgreSQL drivers with PGAdapter and using native Cloud Spanner drivers and client libraries.

## Insights
See [OpenTelemetry in PGAdapter](docs/open_telemetry.md) for how to use `OpenTelemetry` to collect
and export traces to Google Cloud Trace.

## Usage
PGAdapter can be started both as a Docker container, a standalone process as well as an
in-process server (the latter is only supported for Java and other JVM-based applications).
Expand Down
3 changes: 3 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ You can run [these latency comparison benchmarks](../benchmarks/latency-comparis
the difference between using PostgreSQL drivers with PGAdapter and using native Cloud Spanner drivers
and client libraries.

### How can I get insights into execution times in PGAdapter?
PGAdapter supports [using OpenTelemetry](open_telemetry.md) to collect and export traces to Google Cloud Trace.

## Connection Options

### How can I specify the credentials that should be used for a connection?
Expand Down
Binary file added docs/img/dml_batch_trace_sample.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/dml_trace_sample.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/query_trace_sample.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/trace_filter_connection_id.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/trace_filter_retry_attempt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/trace_filter_transaction_id.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
176 changes: 176 additions & 0 deletions docs/open_telemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Open Telemetry in PGAdapter

PGAdapter supports Open Telemetry tracing. You can enable this by adding the `-enable_otel` command
line argument when starting PGAdapter.

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
argument, all traces will be collected and exported. The ratio must be in the range `[0.0, 1.0]`.

Example:

```shell
docker run \
-d -p 5432:5432 \
-v /path/to/credentials.json:/credentials.json:ro \
gcr.io/cloud-spanner-pg-adapter/pgadapter \
-p my-project -i my-instance \
-c /credentials.json -x \
-enable_otel -otel_trace_ratio=0.1
```

## Exporter
PGAdapter will by default export traces to [Google Cloud Trace](https://cloud.google.com/trace).
PGAdapter uses the [OpenTelemetry SDK Autoconfigure module](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md)
for configuring OpenTelemetry. You can use the options of this module to configure another exporter.

PGAdapter uses a different set of default values for the following OpenTelemetry Autoconfigure
properties:
- `otel.traces.exporter`: `none`
- `otel.metrics.exporter`: `none`
- `otel.logs.exporter`: `none`

In addition, PGAdapter always adds an exporter for Google Cloud Trace.

## Traces

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add examples for metrics also here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't add any metrics in this pull request, only traces.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright sounds good then. Thanks!


The traces are by default exported to [Google Cloud Trace](https://cloud.google.com/trace). The
traces follow the structure of the PostgreSQL wire-protocol. Traces typically consist of the
following spans:

1. `query_protocol_handler`: The [PostgreSQL wire-protocol](https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY)
consists of multiple messages to parse, describe, bind, and execute SQL statements. Clients and
drivers will send multiple of these to PGAdapter, and PGAdapter will translate these into a set
of RPC invocations on CLoud Spanner. PGAdapter minimizes the number of RPC invocations that is
needed to respond to these messages. One span of `query_protocol_handler` consists of all the
round-trips that PGAdapter needed to respond to the messages that it received during one query
session.
2. `execute` / `analyze` / `execute_batch`: PGAdapter will translate the incoming messages into
`execute`, `analyze`, and `execute_batch` actions on Cloud Spanner. One span of
`query_protocol_handler` normally consists of one of these actions.
3. `execute_on_spanner`: This span contains the time when PGAdapter hands off the execution of a
statement to the Cloud Spanner Java client, and is roughly identical to the end-to-end
execution of the statement. Note that an `execute_on_spanner` span inside a `execute_batch` span
only buffers the statement client-side. The actual RPC execution time is recorded in the
`execute_batch` span.

### Examples

The following sections show samples of commonly executed statements. All spans that are exported by
Cloud Spanner contain the following attributes:
1. `db.statement`: The SQL statement that is being executed.
2. `pgadapter.connection_id`: Each connection in PGAdapter is assigned a random ID. You can use this
ID to filter spans for a specific connection.
3. `pgadapter.transaction_id`: Each transaction in PGAdapter is assigned a random ID. YOu can use
this ID to filter spans for a specific transaction. This field is not present for statements that
are executed outside an (explicit) transaction.

#### Query (SELECT)

A typical execution of a query consists of the following spans:
1. `query_protocol_handler`: The front-end query protocol handler of PGAdapter that receives
messages from the client and returns results to the client. The query handler normally receives
five messages from the client when executing a query:
1. `P` (Parse): Parse the query string.
2. `B` (Bind): Bind the query parameter values.
3. `D` (Describe): Describe the query result (which columns will be returned by the query).
4. `E` (Execute): Execute the statement and return the results.
5. `S` (Sync): Flush all buffered messages and return all results.
2. `execute`: The front-end query protocol handler requests the backend connection to Cloud Spanner
to execute the query that it constructed from the messages it received.
3. `execute_on_spanner`: This the actual RPC invocation on Cloud Spanner.

![PGAdapter Cloud Trace - Query example](img/query_trace_sample.png?raw=true "PGAdapter Cloud Trace - Query example")

The `query_protocol_handler` span also includes the time it takes to send the results to the client.
This time will also depend on the time that the client requires to actually receive and process the
results. You can see this time in the example above as the time that the `query_protocol_handler`
span is active after the statement has been executed.

#### DML (INSERT/UPDATE/DELETE)

A typical execution of a DML statement consists of the following spans:
1. `query_protocol_handler`: The front-end query protocol handler of PGAdapter that receives
messages from the client and returns results to the client. The query handler normally receives
five messages from the client when executing a DML statement:
1. `P` (Parse): Parse the DML string.
2. `B` (Bind): Bind the DML parameter values.
3. `D` (Describe): Describe the result of the statement. For DML statement, this is an empty result.
4. `E` (Execute): Execute the statement and return the update count.
5. `S` (Sync): Flush all buffered messages and return all results.
2. `execute`: The front-end query protocol handler requests the backend connection to Cloud Spanner
to execute the DML statement that it constructed from the messages it received.
3. `execute_on_spanner`: This the actual RPC invocation on Cloud Spanner.

![PGAdapter Cloud Trace - DML example](img/dml_trace_sample.png?raw=true "PGAdapter Cloud Trace - DML example")

The `query_protocol_handler` span also includes the time it takes to send the update count to the
client. This time is negligible compared to sending a large result set from PGAdapter to the client.

#### Batch

The PostgreSQL wire-protocol also supports batching multiple statements into one execution. This is
done by sending multiple `E` (Execute) messages to the server (PGAdapter) without sending an `S`
(Sync) message. This will allow the server to buffer the statements and wait with sending the
results of these until it sees an `S` (Sync) message. A typical execution of a batch of DML
statements consists of the following spans:
1. `query_protocol_handler`: The front-end query protocol handler of PGAdapter that receives
messages from the client and returns results to the client. The query handler normally receives
four messages for each DML statement in a batch from the client:
1. `P` (Parse): Parse the DML string.
2. `B` (Bind): Bind the DML parameter values.
3. `D` (Describe): Describe the result of the statement. For DML statement, this is an empty result.
4. `E` (Execute): Execute the statement and return the update count.
2. It then receives an `S` (Sync) message when the client has sent all the DML statements.
3. `execute_batch`: The front-end query protocol handler requests the backend connection
to create a DML batch, buffer the DML statements on the client, and then execute the statements
on Cloud Spanner. The total time of this span is the time it took to buffer all the statements on
the client and to execute the batch on Cloud Spanner.
4. `execute_on_spanner`: For each DML statement, an `execute_on_spanner` span is created within the
DML batch. These statements are not executed on spanner, but buffered in the Spanner client until
all the DML statements have been collected. The DML statements are then sent to Cloud Spanner as
one Batch DML request. These spans contain the actual SQL statements that are executed.

![PGAdapter Cloud Trace - Batch DML example](img/dml_batch_trace_sample.png?raw=true "PGAdapter Cloud Trace - Batch DML example")

## Frequently Asked Questions

#### How can I find all the statements that were executed on the same connection as the trace I'm looking at?

All traces include an attribute `pgadapter.connection_id`. This connection ID is randomly chosen,
but remains the same for a connection during its lifetime. You can use this ID to look up all other
traces for the same connection by adding a filter `pgadapter.connection_id:<id>`.

![PGAdapter Cloud Trace - Filter connection id](img/trace_filter_connection_id.png?raw=true "PGAdapter Cloud Trace - Filter connection id")

#### How can I find all the statements in the same transaction?

All traces for statements that used an explicit transaction include an attribute `pgadapter.transaction_id`.
This transaction ID is randomly chosen, but remains the same for a transaction during its lifetime.
You can use this ID to look up all other traces for the same transaction by adding a filter `pgadapter.transaction_id:<id>`.

![PGAdapter Cloud Trace - Filter transaction id](img/trace_filter_transaction_id.png?raw=true "PGAdapter Cloud Trace - Filter transaction id")

#### How can I find statements that waited for an aborted read/write transaction to retry?

Cloud Spanner can abort any read/write transaction. A transaction can be aborted for various
reasons:
- Lock conflicts
- Database schema changes
- Too long inactivity (a read/write transaction is aborted after 10 seconds of inactivity)

But it can also be aborted for internal technical reasons in Cloud Spanner. PGAdapter will attempt
to retry transactions that are aborted by Cloud Spanner. This can cause some statements to seem to
execute much slower than they should, as they have to wait for the internal retry attempt to finish
first. You can find statements that waited for a transaction retry by adding the filter
`HasLabel:pgadapter.retry_attempt`.

![PGAdapter Cloud Trace - Filter for retried transactions](img/trace_filter_retry_attempt.png?raw=true "PGAdapter Cloud Trace - Filter for retried transactions")

Note from the above screenshot:
1. The retry internal retry is initiated and executed as part of the statement execution. This makes
it seem like the statement itself takes long to execute.
2. Cloud Spanner also sends the client a back-off value that it should wait before retrying the
transaction. This is the reason that the total execution time of the statements seems to be much
longer than the transaction retry, as the Java client library will wait for this back-off time
before actually starting the retry.
46 changes: 46 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-bom</artifactId>
<version>1.31.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Expand All @@ -105,6 +112,45 @@
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-spanner</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-metrics</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-logging</artifactId>
</dependency>
<dependency>
<!-- Not managed by opentelemetry-bom -->
<groupId>io.opentelemetry.semconv</groupId>
<artifactId>opentelemetry-semconv</artifactId>
<version>1.22.0-alpha</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-extension-autoconfigure-spi</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud.opentelemetry</groupId>
<artifactId>exporter-trace</artifactId>
<version>0.15.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
Expand Down Expand Up @@ -123,6 +124,10 @@ public class ConnectionHandler extends Thread {
// Separate the following from the threat ID generator, since PG connection IDs are maximum
// 32 bytes, and shouldn't be incremented on failed startups.
private static final AtomicInteger incrementingConnectionId = new AtomicInteger(0);

/** Randomly generated UUID that is included in tracing to identify a connection. */
private final UUID traceConnectionId = UUID.randomUUID();

private ConnectionMetadata connectionMetadata;
private WireMessage message;
private int invalidMessagesCount;
Expand Down Expand Up @@ -708,6 +713,10 @@ public int getSecret() {
return this.secret;
}

public UUID getTraceConnectionId() {
return traceConnectionId;
}

public void setMessageState(WireMessage message) {
this.message = this.server.recordMessage(message);
}
Expand Down
27 changes: 19 additions & 8 deletions src/main/java/com/google/cloud/spanner/pgadapter/ProxyServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package com.google.cloud.spanner.pgadapter;


import com.google.api.core.AbstractApiService;
import com.google.api.core.InternalApi;
import com.google.cloud.spanner.ErrorCode;
Expand All @@ -26,6 +27,7 @@
import com.google.cloud.spanner.pgadapter.statements.IntermediateStatement;
import com.google.cloud.spanner.pgadapter.wireprotocol.WireMessage;
import com.google.common.collect.ImmutableList;
import io.opentelemetry.api.OpenTelemetry;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
Expand Down Expand Up @@ -54,6 +56,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 Properties properties;
private final List<ConnectionHandler> handlers = Collections.synchronizedList(new LinkedList<>());

Expand All @@ -78,29 +81,33 @@ public class ProxyServer extends AbstractApiService {
private final ConcurrentLinkedQueue<WireMessage> debugMessages = new ConcurrentLinkedQueue<>();
private final AtomicInteger debugMessageCount = new AtomicInteger();

ProxyServer(OptionsMetadata optionsMetadata) {
this(optionsMetadata, OpenTelemetry.noop());
}

/**
* Instantiates the ProxyServer from CLI-gathered metadata.
*
* @param optionsMetadata Resulting metadata from CLI.
* @param openTelemetry The {@link OpenTelemetry} to use to collect metrics
*/
public ProxyServer(OptionsMetadata optionsMetadata) {
this.options = optionsMetadata;
this.localPort = optionsMetadata.getProxyPort();
this.properties = new Properties();
this.debugMode = optionsMetadata.isDebugMode();
addConnectionProperties();
public ProxyServer(OptionsMetadata optionsMetadata, OpenTelemetry openTelemetry) {
this(optionsMetadata, openTelemetry, new Properties());
}

/**
* Instantiates the ProxyServer from metadata and properties. For use with in-process invocations.
*
* @param optionsMetadata Resulting metadata from CLI.
* @param properties Properties for specificying additional information to JDBC like an external
* @param openTelemetry The {@link OpenTelemetry} to use to collect metrics
* @param properties Properties for specifying additional information to JDBC like an external
* channel provider (see ConnectionOptions in Java Spanner client library for more details on
* supported properties).
*/
public ProxyServer(OptionsMetadata optionsMetadata, Properties properties) {
public ProxyServer(
OptionsMetadata optionsMetadata, OpenTelemetry openTelemetry, Properties properties) {
this.options = optionsMetadata;
this.openTelemetry = openTelemetry;
this.localPort = optionsMetadata.getProxyPort();
this.properties = properties;
this.debugMode = optionsMetadata.isDebugMode();
Expand Down Expand Up @@ -344,6 +351,10 @@ public OptionsMetadata getOptions() {
return this.options;
}

public OpenTelemetry getOpenTelemetry() {
return this.openTelemetry;
}

/** @return the JDBC connection properties that are used by this server */
public Properties getProperties() {
return (Properties) this.properties.clone();
Expand Down
Loading
Loading