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

tracing: update OpenTelemetry dependencies from 2021 to 2024 #1199

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
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()
35 changes: 30 additions & 5 deletions google/cloud/spanner_v1/_opentelemetry_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,39 @@

from contextlib import contextmanager

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)
odeke-em marked this conversation as resolved.
Show resolved Hide resolved


@contextmanager
def trace_call(name, session, extra_attributes=None):
Expand All @@ -35,14 +57,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 All @@ -52,9 +76,10 @@ def trace_call(name, session, extra_attributes=None):
name, kind=trace.SpanKind.CLIENT, attributes=attributes
) as span:
try:
span.set_status(Status(StatusCode.OK))
yield span
except GoogleAPICallError as error:
span.set_status(Status(StatusCode.ERROR))
except Exception as error:
span.set_status(Status(StatusCode.ERROR, str(error)))
span.record_exception(error)
raise
else:
span.set_status(Status(StatusCode.OK))
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@
]
extras = {
"tracing": [
"opentelemetry-api >= 1.1.0",
"opentelemetry-sdk >= 1.1.0",
"opentelemetry-instrumentation >= 0.20b0, < 0.23dev",
"opentelemetry-api >= 1.22.0",
"opentelemetry-sdk >= 1.22.0",
"opentelemetry-semantic-conventions >= 0.43b0",
],
"libcst": "libcst >= 0.2.5",
}
Expand Down
6 changes: 3 additions & 3 deletions testing/constraints-3.7.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ 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.22.0
opentelemetry-sdk==1.22.0
opentelemetry-semantic-conventions==0.43b0
protobuf==3.20.2
deprecated==1.2.14
grpc-interceptor==0.15.4
21 changes: 21 additions & 0 deletions tests/_helpers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import unittest
import mock

from google.cloud.spanner_v1 import gapic_version

LIB_VERSION = gapic_version.__version__

try:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
from opentelemetry.semconv.attributes.otel_attributes import (
OTEL_SCOPE_NAME,
OTEL_SCOPE_VERSION,
)

from opentelemetry.trace.status import StatusCode

trace.set_tracer_provider(TracerProvider())
Expand All @@ -30,6 +39,18 @@ def get_test_ot_exporter():
return _TEST_OT_EXPORTER


def enrich_with_otel_scope(attrs):
"""
This helper enriches attrs with OTEL_SCOPE_NAME and OTEL_SCOPE_VERSION
for the purpose of avoiding cumbersome duplicated imports.
"""
if HAS_OPENTELEMETRY_INSTALLED:
attrs[OTEL_SCOPE_NAME] = "cloud.google.com/python/spanner"
attrs[OTEL_SCOPE_VERSION] = LIB_VERSION

return attrs


def use_test_ot_exporter():
global _TEST_OT_PROVIDER_INITIALIZED

Expand Down
2 changes: 2 additions & 0 deletions tests/system/test_session_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,8 @@ def _make_attributes(db_instance, **kwargs):
"net.host.name": "spanner.googleapis.com",
"db.instance": db_instance,
}
ot_helpers.enrich_with_otel_scope(attributes)

attributes.update(kwargs)

return attributes
Expand Down
Loading