Skip to content

feat(core): add request hook to enrich T4 network spans with GCP resource attributes (E) - #18272

Draft
chalmerlowe wants to merge 5 commits into
feat/otel-tracing-t3-method-spansfrom
feat/otel-tracing-t4-resource-attributes
Draft

feat(core): add request hook to enrich T4 network spans with GCP resource attributes (E)#18272
chalmerlowe wants to merge 5 commits into
feat/otel-tracing-t3-method-spansfrom
feat/otel-tracing-t4-resource-attributes

Conversation

@chalmerlowe

@chalmerlowe chalmerlowe commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Problem

Low-level gRPC transport spans generated by opentelemetry-instrumentation-grpc capture standard RPC metadata but lack Google Cloud semantic context and resource identity. Without request- and response-level enrichment, spans cannot identify the target GCP resource, normalized string status codes (rpc.response.status_code), or configured endpoint attributes (server.address, server.port).

Solution

This PR enriches wire-level gRPC client spans in google-api-core by attaching request and response hooks to both synchronous and asynchronous OpenTelemetry gRPC interceptors:

  1. Request Hook (_grpc_client_request_hook):

    • Captures gcp.grpc.resend_count when present on retryable requests.
    • Sets rpc.system.name: "grpc" per OpenTelemetry semantic conventions.
    • Extracts server.address and server.port from client_options.api_endpoint.
    • Extracts gcp.resource.destination.id from request.name or request.parent following standard Google Cloud resource naming conventions.
  2. Response Hook (_grpc_client_response_hook):

    • Translates numeric gRPC status codes to canonical string names (e.g. "OK", "NOT_FOUND", "UNAVAILABLE") on rpc.response.status_code.
    • On RPC failures, populates error.type and status.message from response details.
  3. Transport Integration:

    • Registers both hooks in get_otel_interceptor (sync) and get_otel_async_interceptor (async) within google.api_core._observability.

Notes for Reviewers

  • Hooks safely no-op when the active span is not recording, avoiding overhead when tracing is disabled or unsampled.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chalmerlowe
chalmerlowe force-pushed the feat/otel-tracing-t4-resource-attributes branch from a7be0e4 to 9efe757 Compare September 3, 2026 14:49
@chalmerlowe
chalmerlowe changed the base branch from feat/otel-tracing-transport-logic to feat/otel-tracing-t3-method-spans September 3, 2026 14:50
@chalmerlowe chalmerlowe added this to the [o11y] Tracing milestone Sep 3, 2026
@chalmerlowe chalmerlowe changed the title feat(core): add request hook to inject GCP resource and project attributes into OpenTelemetry spans feat(core): add OpenTelemetry T4 span wrapping (E) Sep 3, 2026
@chalmerlowe chalmerlowe self-assigned this Sep 3, 2026
@chalmerlowe chalmerlowe changed the title feat(core): add OpenTelemetry T4 span wrapping (E) feat(core): add request hook to enrich spans with GCP resource attributes (E) Sep 3, 2026
@chalmerlowe
chalmerlowe force-pushed the feat/otel-tracing-t4-resource-attributes branch from 9efe757 to 40071ff Compare September 3, 2026 15:25
@chalmerlowe
chalmerlowe force-pushed the feat/otel-tracing-t4-resource-attributes branch from 40071ff to 8c9d21a Compare September 4, 2026 09:04
@chalmerlowe
chalmerlowe force-pushed the feat/otel-tracing-t4-resource-attributes branch from 8c9d21a to 558f8fd Compare September 4, 2026 09:13
@chalmerlowe chalmerlowe changed the title feat(core): add request hook to enrich spans with GCP resource attributes (E) feat(core): add request hook to enrich T4 network spans with GCP resource attributes (E) Sep 4, 2026
@chalmerlowe
chalmerlowe force-pushed the feat/otel-tracing-t4-resource-attributes branch from 558f8fd to a6f3b3a Compare September 4, 2026 11:49
@chalmerlowe
chalmerlowe force-pushed the feat/otel-tracing-t4-resource-attributes branch from a6f3b3a to 7e1e498 Compare September 4, 2026 14:55
@chalmerlowe
chalmerlowe force-pushed the feat/otel-tracing-t4-resource-attributes branch from 7e1e498 to 131a80a Compare September 4, 2026 17:41
@chalmerlowe
chalmerlowe force-pushed the feat/otel-tracing-t4-resource-attributes branch from 131a80a to 0da0216 Compare September 8, 2026 19:06
@chalmerlowe
chalmerlowe force-pushed the feat/otel-tracing-t4-resource-attributes branch from 0da0216 to 1c3834e Compare September 9, 2026 08:24
try:
attrs["server.port"] = int(port_str)
except ValueError:
attrs["server.port"] = 443

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

QUESTION: is 443 a reasonable default OR should we default to "not given" or something similar?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think assuming 443 could be risky, especially since you're parsing http:// endpoints above

Do you know how Java is managing this? This may be a question for Blake

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked Jetski, and it said Java may be omitting it if it can't parse one. And it and pointed out that the Otel spec says "If using a port other than the default port for this service."

)


def test_extract_endpoint_attributes():

@chalmerlowe chalmerlowe Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test (test_extract_endpoint_attributes) is functional, but I don't like it.
If we agree on the approach in this PR, I am happy to revisit these tests to make them more concise, parametrized, etc.

… 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
@chalmerlowe
chalmerlowe force-pushed the feat/otel-tracing-t4-resource-attributes branch from de053b3 to 051b3d7 Compare September 9, 2026 17:13
14: "UNAVAILABLE",
15: "DATA_LOSS",
16: "UNAUTHENTICATED",
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the standard grpc.StatusCode enum throughout this file? We shouldn't need to manage these ourselves

Is this because grpc is an optional dependency?

_grpc_client_request_hook = _make_grpc_client_request_hook()


def _grpc_client_response_hook(span: Any, response: Any) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you been able to test this with a client?

I was curious about the typing here, so I tried to look it up, but I'm pretty confused. Gemini is telling me that the response is a protobuf message, and this is only called on successful requests. And it found this open issue, saying it may be passing details for async requests. I'm having a hard time finding official docs around this. Are you sure it can complish what we need?

try:
attrs["server.port"] = int(port_str)
except ValueError:
attrs["server.port"] = 443

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think assuming 443 could be risky, especially since you're parsing http:// endpoints above

Do you know how Java is managing this? This may be a question for Blake

endpoint = getattr(client_options, "api_endpoint", None)

if endpoint and isinstance(endpoint, str):
clean = endpoint.replace("http://", "").replace("https://", "").strip("/")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

attrs["gcp.grpc.resend_count"] = resend_count

name = getattr(request, "name", None)
if isinstance(name, str) and name:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

_make_grpc_client_request_hook(endpoint_attrs)
if endpoint_attrs
else _grpc_client_request_hook
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

try:
attrs["server.port"] = int(port_str)
except ValueError:
attrs["server.port"] = 443

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked Jetski, and it said Java may be omitting it if it can't parse one. And it and pointed out that the Otel spec says "If using a port other than the default port for this service."

return False


_STATUS_CODE_NAMES = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@daniel-sanche daniel-sanche left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants