-
Notifications
You must be signed in to change notification settings - Fork 98
feat: add OpenTelemetry tracing to spanner calls #107
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
Changes from 3 commits
2a07883
70a9012
bee0e58
027e0b1
de4b7e7
190f6a7
e78e780
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
Tracing with OpenTelemetry | ||
================================== | ||
Python-spanner uses `OpenTelemetry <https://opentelemetry.io/>`_ to automatically generates traces providing insight on calls to Cloud Spanner. | ||
For information on the benefits and utility of tracing, see the `Cloud Trace docs <https://cloud.google.com/trace/docs/overview>`_. | ||
|
||
To take advantage of these traces, we first need to install opentelemetry: | ||
|
||
.. code-block:: sh | ||
|
||
pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation | ||
|
||
We also need to tell OpenTelemetry which exporter to use. For example, to export python-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.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 | ||
hengfengli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sampler = ProbabilitySampler(1/1000) | ||
# Uses the default tracer provider | ||
hengfengli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
trace.set_tracer_provider(TracerProvider(sampler=sampler)) | ||
trace.get_tracer_provider().add_span_processor( | ||
# initialize the cloud tracing exporter | ||
hengfengli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
BatchExportSpanProcessor(CloudTraceSpanExporter()) | ||
) | ||
|
||
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. | ||
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/>`_ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# Copyright 2020 Google LLC All rights reserved. | ||
# | ||
# 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. | ||
|
||
"""Manages OpenTelemetry trace creation and handling""" | ||
|
||
from contextlib import contextmanager | ||
|
||
from google.api_core.exceptions import GoogleAPICallError | ||
from google.cloud.spanner_v1.gapic import spanner_client | ||
|
||
try: | ||
from opentelemetry import trace | ||
from opentelemetry.trace.status import Status, StatusCanonicalCode | ||
from opentelemetry.instrumentation.utils import http_status_to_canonical_code | ||
|
||
HAS_OPENTELEMETRY_INSTALLED = True | ||
except ImportError: | ||
HAS_OPENTELEMETRY_INSTALLED = False | ||
|
||
|
||
@contextmanager | ||
def trace_call(name, session, extra_attributes=None): | ||
if not HAS_OPENTELEMETRY_INSTALLED or not session: | ||
# empty context manager. users will have to check if the generated value is None or a span | ||
hengfengli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
yield None | ||
return | ||
|
||
tracer = trace.get_tracer(__name__) | ||
|
||
# base attributes that we know for every trace created | ||
hengfengli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
attributes = { | ||
"db.type": "spanner", | ||
"db.url": spanner_client.SpannerClient.SERVICE_ADDRESS, | ||
larkee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"db.instance": session._database.name, | ||
"net.host.name": spanner_client.SpannerClient.SERVICE_ADDRESS, | ||
} | ||
|
||
if extra_attributes: | ||
attributes.update(extra_attributes) | ||
|
||
with tracer.start_as_current_span( | ||
name, kind=trace.SpanKind.CLIENT, attributes=attributes | ||
) as span: | ||
try: | ||
yield span | ||
except GoogleAPICallError as error: | ||
if error.code is not None: | ||
span.set_status(Status(http_status_to_canonical_code(error.code))) | ||
elif error.grpc_status_code is not None: | ||
span.set_status( | ||
# OpenTelemetry's StatusCanonicalCode maps 1-1 with grpc status codes | ||
Status(StatusCanonicalCode(error.grpc_status_code.value[0])) | ||
) | ||
raise |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ | |
from google.cloud.spanner_v1.batch import Batch | ||
from google.cloud.spanner_v1.snapshot import Snapshot | ||
from google.cloud.spanner_v1.transaction import Transaction | ||
from google.cloud.spanner_v1._opentelemetry_tracing import trace_call | ||
import random | ||
|
||
# pylint: enable=ungrouped-imports | ||
|
@@ -114,7 +115,11 @@ def create(self): | |
kw = {} | ||
if self._labels: | ||
kw = {"session": {"labels": self._labels}} | ||
session_pb = api.create_session(self._database.name, metadata=metadata, **kw) | ||
|
||
with trace_call("CloudSpanner.CreateSession", self, self._labels): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder do we have a uniform format for name?
I guess it is okay to use this format because every language has a different format. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was trying to follow Java's naming system when I wrote the doc, but I guess I missed the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Go is a bit special. Can you match it with |
||
session_pb = api.create_session( | ||
self._database.name, metadata=metadata, **kw | ||
) | ||
self._session_id = session_pb.name.split("/")[-1] | ||
|
||
def exists(self): | ||
|
@@ -130,10 +135,16 @@ def exists(self): | |
return False | ||
api = self._database.spanner_api | ||
metadata = _metadata_with_prefix(self._database.name) | ||
try: | ||
api.get_session(self.name, metadata=metadata) | ||
except NotFound: | ||
return False | ||
|
||
with trace_call("CloudSpanner.GetSession", self) as span: | ||
try: | ||
api.get_session(self.name, metadata=metadata) | ||
if span: | ||
span.set_attribute("session_found", True) | ||
except NotFound: | ||
if span: | ||
span.set_attribute("session_found", False) | ||
return False | ||
|
||
return True | ||
|
||
|
@@ -150,8 +161,8 @@ def delete(self): | |
raise ValueError("Session ID not set by back-end") | ||
api = self._database.spanner_api | ||
metadata = _metadata_with_prefix(self._database.name) | ||
|
||
api.delete_session(self.name, metadata=metadata) | ||
with trace_call("CloudSpanner.DeleteSession", self): | ||
api.delete_session(self.name, metadata=metadata) | ||
|
||
def ping(self): | ||
"""Ping the session to keep it alive by executing "SELECT 1". | ||
|
Uh oh!
There was an error while loading. Please reload this page.