Skip to content

Commit

Permalink
tracing: update OpenTelemetry dependencies from 2021 to 2024
Browse files Browse the repository at this point in the history
This change non-invasively introduces dependencies of opentelemetry
bringing in the latest dependencies and modernizing them.

While here also brought in modern span attributes:
* otel.scope.name
* otel.scope.version

Also added a modernized example to produce traces as well
with gRPC-instrumentation enabled, and updated the docs.

Updates googleapis#1170
Fixes googleapis#1173
Built from PR googleapis#1172
  • Loading branch information
odeke-em committed Sep 17, 2024
1 parent e30d23d commit 4c1d4bc
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 17 deletions.
32 changes: 23 additions & 9 deletions docs/opentelemetry-tracing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,46 @@ To take advantage of these traces, we first need to install OpenTelemetry:

.. code-block:: sh
pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation
# [Optional] Installs the cloud monitoring exporter, however you can use any exporter of your choice
pip install opentelemetry-exporter-google-cloud
pip install opentelemetry-api opentelemetry-sdk
pip install opentelemetry-exporter-gcp-trace
We also need to tell OpenTelemetry which exporter to use. To export Spanner traces to `Cloud Tracing <https://cloud.google.com/trace>`_, add the following lines to your application:

.. code:: python
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
# BatchSpanProcessor exports spans to Cloud Trace
# in a seperate thread to not block on the main thread
from opentelemetry.sdk.trace.export import BatchExportSpanProcessor
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# 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(
# Initialize the cloud tracing exporter
BatchExportSpanProcessor(CloudTraceSpanExporter())
BatchSpanProcessor(CloudTraceSpanExporter())
)
To get more fine-grained traces from gRPC, you can enable the gRPC instrumentation by the following

.. code-block:: sh
pip install opentelemetry-instrumentation opentelemetry-instrumentation-grpc
and then in your Python code, please add the following lines:

.. code:: python
from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient
grpc_client_instrumentor = GrpcInstrumentorClient()
grpc_client_instrumentor.instrument()
Generated spanner traces should now be available on `Cloud Trace <https://console.cloud.google.com/traces>`_.

Tracing is most effective when many libraries are instrumented to provide insight over the entire lifespan of a request.
Expand Down
73 changes: 73 additions & 0 deletions examples/grpc_instrumentation_enabled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# -*- 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 BatchSpanProcessor
from opentelemetry.sdk.trace.sampling import ALWAYS_ON
from opentelemetry import trace

# Enable the gRPC instrumentation if you'd like more introspection.
from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient

grpc_client_instrumentor = GrpcInstrumentorClient()
grpc_client_instrumentor.instrument()


def main():
# Setup common variables that'll be used between Spanner and traces.
project_id = os.environ.get('SPANNER_PROJECT_ID', 'test-project')

# Setup OpenTelemetry, trace and Cloud Trace exporter.
tracer_provider = TracerProvider(sampler=ALWAYS_ON)
trace_exporter = CloudTraceSpanExporter(project_id=project_id)
tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter))
trace.set_tracer_provider(tracer_provider)
# Retrieve a tracer from the global tracer provider.
tracer = tracer_provider.get_tracer('MyApp')

# Setup the Cloud Spanner Client.
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('QueryInformationSchema'):
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.
try:
data = snapshot.execute_sql('SELECT CURRENT_TIMESTAMPx()')
for row in data:
print(row)
except Exception as e:
pass


if __name__ == '__main__':
main()
66 changes: 66 additions & 0 deletions examples/trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# -*- 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 BatchSpanProcessor
from opentelemetry.sdk.trace.sampling import ALWAYS_ON
from opentelemetry import trace


def main():
# Setup common variables that'll be used between Spanner and traces.
project_id = os.environ.get('SPANNER_PROJECT_ID', 'test-project')

# Setup OpenTelemetry, trace and Cloud Trace exporter.
tracer_provider = TracerProvider(sampler=ALWAYS_ON)
trace_exporter = CloudTraceSpanExporter(project_id=project_id)
tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter))
trace.set_tracer_provider(tracer_provider)
# Retrieve a tracer from the global tracer provider.
tracer = tracer_provider.get_tracer('MyApp')

# Setup the Cloud Spanner Client.
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('QueryInformationSchema'):
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.
try:
data = snapshot.execute_sql('SELECT CURRENT_TIMESTAMPx()')
for row in data:
print(row)
except Exception as e:
print(e)


if __name__ == '__main__':
main()
27 changes: 26 additions & 1 deletion google/cloud/spanner_v1/_opentelemetry_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,38 @@

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

try:
from opentelemetry import trace
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.semconv.attributes.otel_attributes import (
OTEL_SCOPE_NAME,
OTEL_SCOPE_VERSION,
)

HAS_OPENTELEMETRY_INSTALLED = True
except ImportError:
HAS_OPENTELEMETRY_INSTALLED = False

TRACER_NAME = 'cloud.google.com/python/spanner'
TRACER_VERSION = gapic_version.__version__


def get_tracer(tracer_provider=None):
"""
get_tracer is a utility to unify and simplify retrieval of the tracer, without
leaking implementation details given that retrieving a tracer requires providing
the full qualified library name and version.
When the tracer_provider is set, it'll retrieve the tracer from it, otherwise
it'll fall back to the global tracer provider and use this library's specific semantics.
"""
if not tracer_provider:
# Acquire the global tracer provider.
tracer_provider = trace.get_tracer_provider()

return tracer_provider.get_tracer(TRACER_NAME, TRACER_VERSION)


@contextmanager
def trace_call(name, session, extra_attributes=None):
Expand All @@ -35,14 +58,16 @@ def trace_call(name, session, extra_attributes=None):
yield None
return

tracer = trace.get_tracer(__name__)
tracer = get_tracer()

# 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,
OTEL_SCOPE_NAME: TRACER_NAME,
OTEL_SCOPE_VERSION: TRACER_VERSION,
}

if extra_attributes:
Expand Down
7 changes: 4 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@
]
extras = {
"tracing": [
"opentelemetry-api >= 1.1.0",
"opentelemetry-sdk >= 1.1.0",
"opentelemetry-instrumentation >= 0.20b0, < 0.23dev",
"opentelemetry-api >= 1.24.0",
"opentelemetry-sdk >= 1.24.0",
"opentelemetry-instrumentation >= 0.46b0",
"opentelemetry-semantic-conventions >= 0.46b0",
],
"libcst": "libcst >= 0.2.5",
}
Expand Down
7 changes: 4 additions & 3 deletions testing/constraints-3.7.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ grpc-google-iam-v1==0.12.4
libcst==0.2.5
proto-plus==1.22.0
sqlparse==0.4.4
opentelemetry-api==1.1.0
opentelemetry-sdk==1.1.0
opentelemetry-instrumentation==0.20b0
opentelemetry-api>=1.24.0
opentelemetry-sdk>=1.24.0
opentelemetry-instrumentation==0.46b0
opentelemetry-semantic-conventions==0.46b0
protobuf==3.20.2
deprecated==1.2.14
grpc-interceptor==0.15.4
19 changes: 18 additions & 1 deletion tests/unit/test__opentelemetry_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@
try:
from opentelemetry import trace as trace_api
from opentelemetry.trace.status import StatusCode
from opentelemetry.semconv.attributes import (
OTEL_SCOPE_NAME,
OTEL_SCOPE_VERSION,
)
except ImportError:
pass

from google.api_core.exceptions import GoogleAPICallError
from google.cloud.spanner_v1 import _opentelemetry_tracing
from google.cloud.spanner_v1 import gapic_version

from tests._helpers import OpenTelemetryBase, HAS_OPENTELEMETRY_INSTALLED
from tests._helpers import (
OpenTelemetryBase,
HAS_OPENTELEMETRY_INSTALLED,
)

LIB_VERSION = gapic_version.__version__

def _make_rpc_error(error_cls, trailing_metadata=None):
import grpc
Expand Down Expand Up @@ -59,6 +68,8 @@ def test_trace_call(self):
"db.type": "spanner",
"db.url": "spanner.googleapis.com",
"net.host.name": "spanner.googleapis.com",
OTEL_SCOPE_NAME: "cloud.google.com/python/spanner",
OTEL_SCOPE_VERSION: LIB_VERSION,
}
expected_attributes.update(extra_attributes)

Expand All @@ -84,6 +95,8 @@ def test_trace_error(self):
"db.type": "spanner",
"db.url": "spanner.googleapis.com",
"net.host.name": "spanner.googleapis.com",
OTEL_SCOPE_NAME: "cloud.google.com/python/spanner",
OTEL_SCOPE_VERSION: LIB_VERSION,
}
expected_attributes.update(extra_attributes)

Expand All @@ -110,6 +123,8 @@ def test_trace_grpc_error(self):
"db.type": "spanner",
"db.url": "spanner.googleapis.com:443",
"net.host.name": "spanner.googleapis.com:443",
OTEL_SCOPE_NAME: "cloud.google.com/python/spanner",
OTEL_SCOPE_VERSION: LIB_VERSION,
}
expected_attributes.update(extra_attributes)

Expand All @@ -133,6 +148,8 @@ def test_trace_codeless_error(self):
"db.type": "spanner",
"db.url": "spanner.googleapis.com:443",
"net.host.name": "spanner.googleapis.com:443",
OTEL_SCOPE_NAME: "cloud.google.com/python/spanner",
OTEL_SCOPE_VERSION: LIB_VERSION,
}
expected_attributes.update(extra_attributes)

Expand Down

0 comments on commit 4c1d4bc

Please sign in to comment.