-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(gapic): add OpenTelemetry T3 client method span wrapping in gapic_v1.method (D) #18274
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 all commits
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 |
|---|---|---|
|
|
@@ -22,7 +22,7 @@ | |
| import functools | ||
| from typing import List, Tuple | ||
|
|
||
| from google.api_core import grpc_helpers | ||
| from google.api_core import _observability, grpc_helpers | ||
| from google.api_core.gapic_v1 import client_info | ||
| from google.api_core.timeout import TimeToDeadlineTimeout | ||
|
|
||
|
|
@@ -186,6 +186,42 @@ def __call__( | |
| if self._compression is not None: | ||
| kwargs["compression"] = compression | ||
|
|
||
| if _observability.is_otel_capabilities_enabled(): | ||
| try: | ||
| from opentelemetry import trace | ||
|
|
||
| tracer = trace.get_tracer("google.api_core") | ||
|
Contributor
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. how does this connect with the client's tracer provider? Is that coming later? |
||
| raw_method = getattr(self._target, "_method", None) | ||
|
Contributor
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. have you tested this against a real client yet? IIRC, there are multiple layers of wrapping, so this may not be exposed the way you expect
Contributor
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 if it would be possible to pass down a method name, instead of trying to extract it? The generator already knows it when calling _prep_wrapped_messages |
||
| if raw_method and isinstance(raw_method, (str, bytes)): | ||
| if isinstance(raw_method, bytes): | ||
| raw_method = raw_method.decode("utf-8") | ||
| method_str = raw_method.lstrip("/") | ||
| service, _, method = method_str.rpartition("/") | ||
| span_name = method_str | ||
| else: | ||
| service = "google.api_core" | ||
| method = getattr(self._target, "__name__", "call") | ||
| span_name = f"{service}/{method}" | ||
|
Contributor
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. This is the hot path that is called on every rpc. It seems like some of this would be doing the same (possibly slow) calculation on each invocation, right? Can we move that logic into the one-time init call? |
||
|
|
||
|
Contributor
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. helper methods would be useful here |
||
| with tracer.start_as_current_span( | ||
| span_name, | ||
| kind=trace.SpanKind.CLIENT, | ||
| attributes={ | ||
| "rpc.system": "grpc", | ||
|
Contributor
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. IIRC, this wrapper is also used by HTTP. So we should try to gate this for now |
||
| "rpc.service": service, | ||
| "rpc.method": method, | ||
| }, | ||
| ) as span: | ||
| try: | ||
| return wrapped_func(*args, **kwargs) | ||
|
Contributor
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. There are two places this function can call into wrapped_function. If errors line up the wrong way, it could hit both. We need to be extra careful to avoid double invocation here, because that would be a very serious bug It might be better to call wrapped_func a single time at the end of the method, but use a no-op context manager instead of the tracer if we can't get one
Contributor
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. Note that this span won't be very meaningful for streaming rpcs, because it just tracks the stream set-up, not any of the data flow. I remember asking Wes about streaming, and he said it's out of scope. We should check with Blake if he wants to track stream init like this, or if we should avoid recording any data for streaming rpcs |
||
| except Exception as exc: | ||
| span.record_exception(exc) | ||
| span.set_status(trace.StatusCode.ERROR, str(exc)) | ||
| raise | ||
| # If OpenTelemetry cannot be imported in the current environment, continue without tracing. | ||
| except ImportError: # pragma: NO COVER | ||
| pass | ||
|
Comment on lines
+189
to
+223
Contributor
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. Critical Reliability & Correctness Issues
SolutionWe can use a state flag ( if _observability.is_otel_capabilities_enabled():
func_called = False
try:
from opentelemetry import trace
tracer = trace.get_tracer("google.api_core")
raw_method = getattr(self._target, "_method", None)
if raw_method and isinstance(raw_method, (str, bytes)):
if isinstance(raw_method, bytes):
raw_method = raw_method.decode("utf-8")
method_str = raw_method.lstrip("/")
service, _, method = method_str.rpartition("/")
span_name = method_str
else:
service = "google.api_core"
method = getattr(self._target, "__name__", "call")
span_name = f"{service}/{method}"
with tracer.start_as_current_span(
span_name,
kind=trace.SpanKind.CLIENT,
attributes={
"rpc.system": "grpc",
"rpc.service": service,
"rpc.method": method,
},
) as span:
try:
func_called = True
return wrapped_func(*args, **kwargs)
except Exception as exc:
span.record_exception(exc)
span.set_status(trace.StatusCode.ERROR, str(exc))
raise
except Exception:
if func_called:
raiseReferences
|
||
|
|
||
| return wrapped_func(*args, **kwargs) | ||
|
|
||
|
|
||
|
|
||
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.
Do we need to create this on every invocation? Can we cache it for each request? Or even use a singleton shared across all instances?