Skip to content

Commit

Permalink
fix: use modernized and standardized OpenTelemetry when tracing
Browse files Browse the repository at this point in the history
This change modernizes trace span attributes by using OpenTelemetry's
semantic conventions that are standardized and allow for much
better common ground adoption by broader systems, even more as
Google Cloud Tracing & Monitoring pushes towards OpenTelemetry more.

With this change we've made the replacement of these fields, directly
with imports from `opentelemetry.semconv.trace.SpanAttributes`, as:

* "db.type"       => DB_SYSTEM aka "db.system"
* "db.url"        => DB_CONNECTION_STRING aka "db.connection_string"
* "db.instance"   => DB_NAME aka "db.name"
* "net.host.name" => NET_HOST_NAME aka "net.host.name"

While here, also updated opentelemetry-(api, sdk) dependencies to use
versions "1.25.0", then opentelemetry-(instrumentation) to "0.46b0"

Also added in an option toggled by environment variable

    ENABLE_EXTENDED_TRACING=true

which allows spans to be annotated with the SQL statement keyed by

    "db.statement"

Fixes googleapis#1170
Fixes googleapis#1171
Fixes googleapis#1173
  • Loading branch information
odeke-em committed Aug 9, 2024
1 parent 55f83dc commit a62ee3e
Show file tree
Hide file tree
Showing 15 changed files with 282 additions and 127 deletions.
8 changes: 6 additions & 2 deletions docs/opentelemetry-tracing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ To take advantage of these traces, we first need to install OpenTelemetry:
.. code-block:: sh
pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation
pip install opentelemetry-exporter-google-cloud
# [Optional] Installs the cloud monitoring exporter, however you can use any exporter of your choice
pip install opentelemetry-exporter-google-cloud
Expand All @@ -19,14 +20,14 @@ We also need to tell OpenTelemetry which exporter to use. To export Spanner trac
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.trace.sampling import ProbabilitySampler
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
# BatchExportSpanProcessor exports spans to Cloud Trace
# in a seperate thread to not block on the main thread
from opentelemetry.sdk.trace.export import BatchExportSpanProcessor
# Create and export one trace every 1000 requests
sampler = ProbabilitySampler(1/1000)
sampler = TraceIdRatioBased(1/1000)
# Use the default tracer provider
trace.set_tracer_provider(TracerProvider(sampler=sampler))
trace.get_tracer_provider().add_span_processor(
Expand All @@ -38,3 +39,6 @@ Generated spanner traces should now be available on `Cloud Trace <https://consol

Tracing is most effective when many libraries are instrumented to provide insight over the entire lifespan of a request.
For a list of libraries that can be instrumented, see the `OpenTelemetry Integrations` section of the `OpenTelemetry Python docs <https://opentelemetry-python.readthedocs.io/en/stable/>`_

To allow for SQL statements to be annotated in your spans, please set
the environment variable `SPANNER_ENABLE_EXTENDED_TRACING=true`.
61 changes: 61 additions & 0 deletions examples/trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# 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

import os
import time

import google.cloud.spanner as spanner
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchExportSpanProcessor
from opentelemetry.sdk.trace.sampling import ALWAYS_ON
from opentelemetry import trace


def main():
# Setup OpenTelemetry, trace and Cloud Trace exporter.
sampler = ALWAYS_ON
tracerProvider = TracerProvider(sampler=sampler)
tracerProvider.add_span_processor(
BatchExportSpanProcessor(CloudTraceSpanExporter()))
trace.set_tracer_provider(tracerProvider)
tracer = trace.get_tracer(__name__)

# Setup the Cloud Spanner Client.
project_id = os.environ.get('SPANNER_PROJECT_ID')
spanner_client = spanner.Client(project_id)
instance = spanner_client.instance('test-instance')
database = instance.database('test-db')

# Now run our queries
with tracer.start_as_current_span('QueryDatabase'):
with database.snapshot() as snapshot:
with tracer.start_as_current_span('InformationSchema'):
info_schema = snapshot.execute_sql(
'SELECT * FROM INFORMATION_SCHEMA.TABLES')
for row in info_schema:
print(row)

with tracer.start_as_current_span('ServerTimeQuery'):
with database.snapshot() as snapshot:
# Purposefully issue a bad SQL statement to examine exceptions
# that get recorded and a ERROR span status.
data = snapshot.execute_sql('SELECT CURRENT_TIMESTAMPx()')
for row in data:
print(row)


if __name__ == '__main__':
main()
31 changes: 27 additions & 4 deletions google/cloud/spanner_v1/_opentelemetry_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,39 @@
"""Manages OpenTelemetry trace creation and handling"""

from contextlib import contextmanager
import os

from google.api_core.exceptions import GoogleAPICallError
from google.cloud.spanner_v1 import SpannerClient

try:
from opentelemetry import trace
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.semconv.trace import SpanAttributes

HAS_OPENTELEMETRY_INSTALLED = True
DB_SYSTEM = SpanAttributes.DB_SYSTEM
DB_NAME = SpanAttributes.DB_NAME
DB_CONNECTION_STRING = SpanAttributes.DB_CONNECTION_STRING
NET_HOST_NAME = SpanAttributes.NET_HOST_NAME
DB_STATEMENT = SpanAttributes.DB_STATEMENT
except ImportError:
HAS_OPENTELEMETRY_INSTALLED = False


EXTENDED_TRACING_ENABLED = os.environ.get('SPANNER_ENABLE_EXTENDED_TRACING', '') == 'true'


def annotate_with_sql_statement(span, sql):
"""
annotate_sql_statement will set the attribute DB_STATEMENT
to the sql statement, only if SPANNER_ENABLE_EXTENDED_TRACING=true
is set in the environment.
"""
if EXTENDED_TRACING_ENABLED:
span.set_attribute(DB_STATEMENT, sql)


@contextmanager
def trace_call(name, session, extra_attributes=None):
if not HAS_OPENTELEMETRY_INSTALLED or not session:
Expand All @@ -39,15 +59,18 @@ def trace_call(name, session, extra_attributes=None):

# Set base attributes that we know for every trace created
attributes = {
"db.type": "spanner",
"db.url": SpannerClient.DEFAULT_ENDPOINT,
"db.instance": session._database.name,
"net.host.name": SpannerClient.DEFAULT_ENDPOINT,
DB_SYSTEM: "google.cloud.spanner",
DB_CONNECTION_STRING: SpannerClient.DEFAULT_ENDPOINT,
DB_NAME: session._database.name,
NET_HOST_NAME: SpannerClient.DEFAULT_ENDPOINT,
}

if extra_attributes:
attributes.update(extra_attributes)

if not EXTENDED_TRACING_ENABLED:
attributes.pop(DB_STATEMENT, None)

with tracer.start_as_current_span(
name, kind=trace.SpanKind.CLIENT, attributes=attributes
) as span:
Expand Down
9 changes: 6 additions & 3 deletions google/cloud/spanner_v1/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@
_check_rst_stream_error,
_SessionWrapper,
)
from google.cloud.spanner_v1._opentelemetry_tracing import trace_call
from google.cloud.spanner_v1._opentelemetry_tracing import (
trace_call,
DB_STATEMENT,
)
from google.cloud.spanner_v1.streamed import StreamedResultSet
from google.cloud.spanner_v1 import RequestOptions

Expand Down Expand Up @@ -488,7 +491,7 @@ def execute_sql(
timeout=timeout,
)

trace_attributes = {"db.statement": sql}
trace_attributes = {DB_STATEMENT: sql}

if self._transaction_id is None:
# lock is added to handle the inline begin for first rpc
Expand Down Expand Up @@ -696,7 +699,7 @@ def partition_query(
partition_options=partition_options,
)

trace_attributes = {"db.statement": sql}
trace_attributes = {DB_STATEMENT: sql}
with trace_call(
"CloudSpanner.PartitionReadWriteTransaction",
self._session,
Expand Down
Loading

0 comments on commit a62ee3e

Please sign in to comment.