feat(core): add request hook to enrich T4 network spans with GCP resource attributes (E) - #18272
feat(core): add request hook to enrich T4 network spans with GCP resource attributes (E)#18272chalmerlowe wants to merge 16 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces OpenTelemetry request hooks to extract and inject Google Cloud semantic and resource attributes (such as resource name, parent, and project ID) from gRPC request objects into OpenTelemetry spans. Specifically, it adds _extract_t4_attributes and _client_request_hook helper functions in _observability.py, registers the hook in the gRPC client interceptors, and includes comprehensive unit tests to verify this behavior. There are no review comments, so I have no feedback to provide.
a7be0e4 to
9efe757
Compare
b5a883a to
83963a7
Compare
9efe757 to
40071ff
Compare
83963a7 to
bf82825
Compare
40071ff to
8c9d21a
Compare
bf82825 to
bc94977
Compare
8c9d21a to
558f8fd
Compare
bc94977 to
67e3879
Compare
558f8fd to
a6f3b3a
Compare
67e3879 to
6b9bfab
Compare
a6f3b3a to
7e1e498
Compare
fb16695 to
9b46ef7
Compare
7e1e498 to
131a80a
Compare
8c5d285 to
62a8261
Compare
131a80a to
0da0216
Compare
007f6b0 to
5b39cff
Compare
0da0216 to
1c3834e
Compare
5b39cff to
c15d70a
Compare
de053b3 to
051b3d7
Compare
| endpoint = getattr(client_options, "api_endpoint", None) | ||
|
|
||
| if endpoint and isinstance(endpoint, str): | ||
| clean = endpoint.replace("http://", "").replace("https://", "").strip("/") |
There was a problem hiding this comment.
Is there a reason you're avoiding something like urllib.parse to parse this for us?
Manual parsing can be brittle. E.g.m what if this is a ipv6 address? Or there's a path component after the port?
There was a problem hiding this comment.
Replaced the manual approach with urllib.parse. Great catch. Thnaks!
| attrs["gcp.grpc.resend_count"] = resend_count | ||
|
|
||
| name = getattr(request, "name", None) | ||
| if isinstance(name, str) and name: |
There was a problem hiding this comment.
When would you expect this to be non-string? Are you sure we should fall back to the parent in that case?
| else: | ||
| parent = getattr(request, "parent", None) | ||
| if isinstance(parent, str) and parent: | ||
| attrs["gcp.resource.destination.id"] = parent |
There was a problem hiding this comment.
nit: we could reduce some duplication here:
resource_id = getattr(request, "name", None) or getattr(request, "parent", None)
if isinstance(resource_id, str) and resource_id:
attrs["gcp.resource.destination.id"] = resource_id
There was a problem hiding this comment.
This is OBE. We no longer process "gcp.resource.destination.id"
| _make_grpc_client_request_hook(endpoint_attrs) | ||
| if endpoint_attrs | ||
| else _grpc_client_request_hook | ||
| ) |
There was a problem hiding this comment.
nit: can't this just be _make_grpc_client_request_hook(endpoint_attrs)? It seems like the implementation already handles empty endpoint_attrs, so I'm not sure we need to handle both cases here
There was a problem hiding this comment.
Simplified. Good catch. Appreciate it.
| return False | ||
|
|
||
|
|
||
| _STATUS_CODE_NAMES = { |
There was a problem hiding this comment.
In the PR description, you say " Without request- and response-level enrichment, spans cannot identify ... normalized string status codes".
Can you expand on that? Does the default instrumenter add code numbers, but not strings? It feels strange that we would have to add the cost of an extra callback layer to do that kind of transformation
Could we add this somewhere else in the stack? Or just stick with error numbers instead of names?
There was a problem hiding this comment.
the PR description was outdated and described an earlier prototype where the response hook was trying to do too much.
To answer your questions directly:
-
Does the default instrumenter add code numbers, but not strings?
Yes. Upstreamopentelemetry-instrumentation-grpcsetsrpc.grpc.status_codeas an integer (0,5, etc.) per OTel gRPC conventions. It does not setrpc.response.status_codeat all. -
Can we do this elsewhere / avoid callback translation overhead?
Yes, we moved error resolution out of this hook. All error status mapping ("NOT_FOUND","RESOURCE_EXHAUSTED", etc.) andgcp.errors.*attributes are handled directly on the client method span ingoogle.api_core.gapic_v1.method(PR feat(gapic): add OpenTelemetry T3 client method span wrapping in gapic_v1.method (D) #18274) where exceptions are caught naturally. -
What
_grpc_client_response_hookactually does in this PR:
The response hook here does zero error parsing, zero translation, and zero dictionary lookups. It only stampsrpc.response.status_code = "OK"on successful wire spans (checkingspan.statusfirst to bail out if an error occurred).
I've updated the PR description to reflect the current, simplified implementation.
daniel-sanche
left a comment
There was a problem hiding this comment.
My main comment is around the response callback. Are you sure it can do what we need? And do we really need a callback for status names?
051b3d7 to
2460284
Compare
4739860 to
ef5771d
Compare
8775743 to
d6307d6
Compare
3610162 to
b1d7f5d
Compare
d6307d6 to
b3931e9
Compare
|
|
||
|
|
||
| def _grpc_client_response_hook(span: Any, response: Any) -> None: | ||
| """OpenTelemetry gRPC client response hook to record response status code. |
There was a problem hiding this comment.
It seems like this is just being used to record sufccessful states now, right? Can we reflect that in the docstring?
There was a problem hiding this comment.
As an aside, I'm a little surprised we'd need to add hooks to do this manually. I'd expect the interceptor to handle this. Gemini says it may be because the interceptor was written against an older version of the spec, and might be improved in the future. So we should make sure the doc strings are clear about what this hook is doing here, in case we can remove it in the future.
|
|
||
| if endpoint and isinstance(endpoint, str): | ||
| target = endpoint if "//" in endpoint else f"//{endpoint}" | ||
| parsed = urllib.parse.urlsplit(target) |
There was a problem hiding this comment.
It looks like this could raise an exception. Can we either put this within the try block, or add a Raises section to the docstring, and make sure any exceptions are handled at a higher level?
There was a problem hiding this comment.
Done: We moved the core steps that could cause an exception (the parsing steps) into the try/except.
| if request is None: | ||
| return attrs | ||
|
|
||
| resend_count = getattr(request, "resend_count", None) |
There was a problem hiding this comment.
My understanding is that request will be a protobuf message, which wouldn't have this field. What do you expect it to be? Can we have any clearer typing here?
|
|
||
| # Remove duplicate legacy rpc.system attribute set by stock instrumentation | ||
| # in favor of modern rpc.system.name ("grpc") per PRD changelog. | ||
| span_attributes = getattr(span, "_attributes", None) |
There was a problem hiding this comment.
Is there a way to do this without reaching into the private API?
It looks like this is done with extensive null checks, so it's probably ok to leave this as-is. But if Otel doesn't provide a way to overwrite this attribute, maybe we should just not touch it
| if span is None or not getattr(span, "is_recording", lambda: True)(): | ||
| return | ||
|
|
||
| # Upstream opentelemetry-instrumentation-grpc names spans with a leading slash |
There was a problem hiding this comment.
I looked into this, and it seems like this isn't part of the formal spec, even if it's how the upstream package is behaving.
It looks like this logic only sets clean_method_name if it has the slash. Can we re-work it so the method name is always captured?
b1d7f5d to
67c8ae7
Compare
… hook - Add rpc.system.name: 'grpc' - Extract server.address and server.port from client options endpoint - Extract gcp.grpc.resend_count from request resend count - Extract gcp.resource.destination.id from request name or parent - Add _client_response_hook for status code, error.type, and status.message - Plumb response_hook into get_otel_interceptor and get_otel_async_interceptor
- Test endpoint attribute parsing across host/port variations - Test destination id and resend count extraction - Test client request and response hooks covering all status and error cases - Test interceptor creation and custom endpoint attribute propagation - Achieve 100% statement and branch coverage on _observability.py
…and hooks - Rename _extract_t4_attributes to _extract_grpc_request_attributes - Rename _make_client_request_hook to _make_grpc_client_request_hook - Rename _client_request_hook to _grpc_client_request_hook - Rename _client_response_hook to _grpc_client_response_hook - Preserve generic _extract_endpoint_attributes for shared transport usage
…ntion - Rename test_extract_t4_attributes to test_extract_grpc_request_attributes - Rename test_client_request_hook to test_grpc_client_request_hook - Rename test_client_response_hook to test_grpc_client_response_hook - Update interceptor hook references to _grpc_client_* hooks
- Add url.domain extraction from universe_domain or default to googleapis.com - Add _extract_error_attributes helper to extract gcp.errors.domain and gcp.errors.metadata.<key> - Omit server.port when port matches scheme defaults (443 for https/grpc, 80 for http) - Remove redundant _grpc_client_response_hook and _STATUS_CODE_NAMES - Deduplicate name and parent resource lookup for gcp.resource.destination.id - Add comprehensive parametrized unit tests and update interceptor test suites
…tem attribute - Strip leading slash from gRPC attempt span names via span.update_name - Set rpc.method to the fully qualified method name per PRD specification - Retain rpc.system.name: 'grpc' and remove legacy rpc.system attribute to avoid duplication - Update unit tests to verify span name normalization and attribute deduplication
- Remove gcp.resource.destination.id extraction from _extract_grpc_request_attributes - Update unit tests to reflect attribute removal per July Strategy Update
…ments without grpc
b3931e9 to
99a4d3d
Compare
…parsing, and attribute handling
Problem
Low-level gRPC transport spans generated by
opentelemetry-instrumentation-grpccapture standard RPC metadata but lack Google Cloud semantic context. Without request- and response-level enrichment, spans cannot identify configured endpoint attributes (server.address,server.port,url.domain), resend count, or the Cloud Observability success status (rpc.response.status_code = "OK").Solution
This PR enriches wire-level gRPC client spans in
google-api-coreby attaching lightweight request and response hooks to both synchronous and asynchronous OpenTelemetry gRPC interceptors:Request Hook (
_make_grpc_client_request_hook):gcp.grpc.resend_countwhen present on retryable requests.rpc.system.name: "grpc"per OpenTelemetry semantic conventions.server.address,server.port(omitted for standard 443/80 ports), andurl.domainfromclient_options.Response Hook (
_grpc_client_response_hook):rpc.response.status_code = "OK"strictly on successful RPC completions.gcp.errors.*attributes are handled at the client method span level).Transport Integration:
get_otel_interceptor(sync) andget_otel_async_interceptor(async) withingoogle.api_core._observability.Notes for Reviewers