-
Notifications
You must be signed in to change notification settings - Fork 87
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
fix: use modernized and standardized OpenTelemetry when tracing #1172
base: main
Are you sure you want to change the base?
Changes from all commits
51caf4e
6a11050
406cff8
1c6188f
9f22b1a
cb6fa39
e7cf4c7
31b6d6d
03a648c
ceb7da2
c32adc8
b190663
58c8555
483fa17
7eca5d5
331bbaf
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,71 @@ | ||
# -*- 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') | ||
|
||
# Setup OpenTelemetry, trace and Cloud Trace exporter. | ||
sampler = ALWAYS_ON | ||
tracerProvider = TracerProvider(sampler=sampler) | ||
traceExporter = CloudTraceSpanExporter(project_id) | ||
tracerProvider.add_span_processor( | ||
BatchSpanProcessor(traceExporter)) | ||
trace.set_tracer_provider(tracerProvider) | ||
# Retrieve the set shared tracer. | ||
tracer = tracerProvider.get_tracer('cloud.google.com/python/spanner', spanner.__version__) | ||
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 dont think this is needed. In this case Can you update the module name and version name to application name and version and see how the output changes? |
||
|
||
# Setup the Cloud Spanner Client. | ||
# Here we directly pass in the tracerProvider into the spanner client. | ||
opts = dict(tracer_provider=tracerProvider) | ||
spanner_client = spanner.Client(project_id, observability_options=opts) | ||
|
||
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. | ||
data = snapshot.execute_sql('SELECT CURRENT_TIMESTAMP()') | ||
for row in data: | ||
print(row) | ||
|
||
if __name__ == '__main__': | ||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# -*- 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') | ||
|
||
# Setup OpenTelemetry, trace and Cloud Trace exporter. | ||
sampler = ALWAYS_ON | ||
tracerProvider = TracerProvider(sampler=sampler) | ||
traceExporter = CloudTraceSpanExporter(project_id) | ||
tracerProvider.add_span_processor( | ||
BatchSpanProcessor(traceExporter)) | ||
trace.set_tracer_provider(tracerProvider) | ||
# Retrieve the set shared tracer. | ||
tracer = tracerProvider.get_tracer('MyPackage') | ||
|
||
# Setup the Cloud Spanner Client. | ||
spanner_client = spanner.Client(project_id) | ||
# Alternatively you can directly pass in the tracerProvider into | ||
# the spanner client, otherwise the global tracer shall be used. | ||
if False: | ||
opts = dict(tracer_provider=tracerProvider) | ||
spanner_client = spanner.Client(project_id, observability_options=opts) | ||
|
||
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. | ||
data = snapshot.execute_sql('SELECT CURRENT_TIMESTAMP()') | ||
for row in data: | ||
print(row) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,39 +15,95 @@ | |
"""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 | ||
from google.cloud.spanner_v1 import gapic_version as LIB_VERSION | ||
|
||
try: | ||
from opentelemetry import trace | ||
from opentelemetry.trace.status import Status, StatusCode | ||
from opentelemetry.semconv.trace import SpanAttributes | ||
from opentelemetry.semconv.attributes import ( | ||
OTEL_SCOPE_NAME, | ||
OTEL_SCOPE_VERSION, | ||
) | ||
|
||
HAS_OPENTELEMETRY_INSTALLED = True | ||
except ImportError: | ||
DB_SYSTEM = SpanAttributes.DB_SYSTEM | ||
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.
So should we make these changes directly or instead protect them under an option? 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.
|
||
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 as e: | ||
HAS_OPENTELEMETRY_INSTALLED = False | ||
DB_STATEMENT = 'db.statement' | ||
|
||
|
||
EXTENDED_TRACING_ENABLED = os.environ.get('SPANNER_ENABLE_EXTENDED_TRACING', '') == 'true' | ||
odeke-em marked this conversation as resolved.
Show resolved
Hide resolved
|
||
LIB_FQNAME = 'cloud.google.com/python/spanner' | ||
TRACER_NAME = LIB_FQNAME | ||
TRACER_VERSION = LIB_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): | ||
def trace_call(name, session, extra_attributes=None, observability_options=None): | ||
if not HAS_OPENTELEMETRY_INSTALLED or not session: | ||
odeke-em marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# Empty context manager. Users will have to check if the generated value is None or a span | ||
yield None | ||
return | ||
|
||
tracer = trace.get_tracer(__name__) | ||
tracer = None | ||
if observability_options: | ||
tracerProvider = observability_options.get('tracer_provider', None) | ||
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. variable should be lower case |
||
if tracerProvider: | ||
tracer = get_tracer(tracerProvider) | ||
|
||
if tracer is None: # Acquire the global tracer if none was provided. | ||
tracer = get_tracer() | ||
|
||
# It is imperative that we properly record the Cloud Spanner | ||
# endpoint tracks whether we are connecting to the emulator | ||
# or to production. | ||
spanner_endpoint = os.getenv("SPANNER_EMULATOR_HOST") | ||
if not spanner_endpoint: | ||
spanner_endpoint = SpannerClient.DEFAULT_ENDPOINT | ||
|
||
# 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: "spanner", | ||
DB_CONNECTION_STRING: spanner_endpoint, | ||
DB_NAME: session._database.name, | ||
NET_HOST_NAME: spanner_endpoint, | ||
OTEL_SCOPE_NAME: LIB_FQNAME, | ||
OTEL_SCOPE_VERSION: TRACER_VERSION, | ||
} | ||
|
||
if extra_attributes: | ||
attributes.update(extra_attributes) | ||
|
||
extended_tracing = EXTENDED_TRACING_ENABLED or ( | ||
observability_options and | ||
observability_options.get('enable_extended_tracing', False)) | ||
|
||
if not extended_tracing: | ||
attributes.pop(DB_STATEMENT, None) | ||
|
||
with tracer.start_as_current_span( | ||
name, kind=trace.SpanKind.CLIENT, attributes=attributes | ||
) as span: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need a seperate job for extended tracing? What are we validating with the help of this job?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's to ensure that extended tracing works as expected because there is no other mechanism to test for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EXTENDED_TRACING_ENABLED will add sql statements in span attributes right?
I don't think we need to run a job explicitly for that if we don't have any tests specifically to validate this behavior in system tests.