-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(observability): [WIP] implement universal 4-path OpenTelemetry tracing #18433
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
Draft
chalmerlowe
wants to merge
56
commits into
main
Choose a base branch
from
feat/otel-tracing-universal-4path
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+14,687
−2,170
Draft
Changes from all commits
Commits
Show all changes
56 commits
Select commit
Hold shift + click to select a range
bfb40b0
feat(core): add request hook to inject GCP resource and project attri…
chalmerlowe 87ca9b8
feat(core): implement complete T4 gRPC telemetry capture and response…
chalmerlowe 451e17b
test(core): add comprehensive unit tests for T4 gRPC telemetry and hooks
chalmerlowe 29de72e
refactor(core): adopt explicit _grpc_* naming for request extraction …
chalmerlowe 7f6519f
test(core): align test names and assertions with _grpc_* naming conve…
chalmerlowe b0de0a4
feat(core): add url.domain, error attributes, and streamline T4 hooks
chalmerlowe ac095c5
feat(core): normalize gRPC span names and eliminate duplicate rpc.sys…
chalmerlowe ef7d77d
refactor(core): remove deferred gcp.resource.destination.id attribute
chalmerlowe aa2bead
feat(core): record rpc.response.status_code on wire attempt spans
chalmerlowe 894b380
refactor(core): remove duplicate error attribute extraction in favor …
chalmerlowe 1c1b9af
fix(observability): resolve mypy union-attr error and support environ…
chalmerlowe 81b686c
refactor(observability): simplify response hook to record OK on succe…
chalmerlowe 324b866
test(observability): cover request hook span edge cases for 100% bran…
chalmerlowe abeaf04
fix(observability): safely handle invalid port in endpoint attributes
chalmerlowe 99a4d3d
fix(observability): ensure response hook only records OK on successfu…
chalmerlowe 4b82c9c
refactor(observability): address review feedback on method name, url …
chalmerlowe 0fb354a
docs(observability): clarify sync vs async behavior and specify semco…
chalmerlowe acce308
feat(gapic): add OpenTelemetry channel tracing to generator templates
chalmerlowe d337933
fix(gapic): resolve CI import errors on unreleased ClientInterceptor …
chalmerlowe 13f1218
fix(gapic): use AnonymousCredentials in test_grpc_transport_channel_i…
chalmerlowe 11ccd0d
test(gapic): update bazel integration goldens for otel channel tracing
chalmerlowe a7dad4f
ci(gapic): add OpenTelemetry test dependencies to showcase nox sessions
chalmerlowe 858ff52
test(gapic): support client_options and otel interceptor in system te…
chalmerlowe b1c66e4
test(gapic): add showcase system test suite for OpenTelemetry channel…
chalmerlowe b409ba6
feat(gapic): broaden transport subclass check and harden tracing tests
chalmerlowe 2059f32
refactor(gapic): guard ClientInterceptor under TYPE_CHECKING in trans…
chalmerlowe 66a6f0e
test(gapic): synchronize NO COVER pragma in golden gRPC transports
chalmerlowe 7ebebfa
test(gapic): support flexible import of construct_client in system tr…
chalmerlowe a7956a5
feat(gapic): resolve OTel interceptor in GrpcTransport and pass clien…
chalmerlowe cd5299a
test(gapic): update bazel integration goldens for transport tracing u…
chalmerlowe 8b540c8
fix(gapic): resolve showcase mypy error and ensure base transport wra…
chalmerlowe 880f5b9
feat(observability): add fallback status code and exception mapping f…
chalmerlowe 9bb3305
test(gapic): harmonize showcase system tracing tests with env gating …
chalmerlowe af22028
fix(observability): ensure 100% branch coverage in error attribute ex…
chalmerlowe d073f2b
perf(generator): cache wrap_method tracing check at module level
chalmerlowe 715f58b
refactor(observability): guard none span in response hook and tag int…
chalmerlowe c13f97a
feat(gapic): harden otel interceptor deduplication and options checki…
chalmerlowe b1b7ce4
test(gapic): update bazel integration goldens for interceptor hardening
chalmerlowe ce65620
Merge remote-tracking branch 'origin/main' into feat/otel-tracing-gap…
chalmerlowe d53607b
fix(ci): synchronize goldens with bazel generator and align with main
chalmerlowe f7f3feb
feat(gapic): wire method_name for mixin methods in base transport
chalmerlowe 2f522bd
test(gapic): clarify test handling of abstract base transport NotImpl…
chalmerlowe ee5f778
refactor(testing): tighten fixture usage and standardize span asserti…
chalmerlowe 89675ea
feat(observability): populate status.message span attribute for cross…
chalmerlowe 8abdbc6
feat(observability): implement universal 4-path OpenTelemetry tracing
chalmerlowe cba76f3
fix(observability): resolve 4-path transport tracing gaps and support…
chalmerlowe 2772ee3
fix(observability): resolve mypy typing, matrix coverage, and update …
chalmerlowe 09080aa
fix(generator): isolate goldens from pre-commit formatting and sync i…
chalmerlowe a933acb
fix(generator): sync Bazel integration goldens with raw generator out…
chalmerlowe 78a1db7
test(tracing): achieve 100% coverage on google-api-core and showcase …
chalmerlowe 0f6e078
refactor(tracing): centralize wrap_method introspection and simplify …
chalmerlowe 2535c2d
refactor(gapic-generator): unify transport method wrapping and centra…
chalmerlowe 1f54064
refactor(observability): add trace_http_request helper and simplify r…
chalmerlowe 4c8984d
refactor(gapic-generator): unify transport wrap helper, clean compat …
chalmerlowe 03e8dff
fix(core): route async channel interceptors by RPC type
chalmerlowe f63234f
refactor(observability): complete docstring typing, enforce transport…
chalmerlowe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -159,30 +159,39 @@ def _get_http_options(): | |
| session, | ||
| timeout, | ||
| transcoded_request, | ||
| body=None): | ||
|
|
||
| body=None, | ||
| client_options=None): | ||
| """Execute the HTTP request over the transport session with | ||
| OpenTelemetry tracing and metadata propagation.""" | ||
|
|
||
| uri = transcoded_request['uri'] | ||
| method = transcoded_request['method'] | ||
| headers = dict(metadata) | ||
| headers['Content-Type'] = 'application/json' | ||
| response = {{ await_prefix }}getattr(session, method)( | ||
| "{host}{uri}".format(host=host, uri=uri), | ||
| timeout=timeout, | ||
| url = "{host}{uri}".format(host=host, uri=uri) | ||
|
|
||
| with trace_http_request( | ||
| client_options=client_options, | ||
| method=method, | ||
| url=url, | ||
| url_template=uri, | ||
| headers=headers, | ||
| params=rest_helpers.flatten_query_params(query_params, strict=True), | ||
| {% if body_spec %} | ||
| data=body, | ||
| {% endif %} | ||
| {% if not is_async and is_streaming_method %} | ||
| {# NOTE: The underlying `requests` library used for making a sync request | ||
| # requires us to set `stream=True` to avoid loading the entire response | ||
| # into memory at once. For an async request, given its nature where it | ||
| # reads data chunk by chunk, this is not required. | ||
| #} | ||
| stream=True, | ||
| {% endif %} | ||
| body=body, | ||
| ) as span: | ||
| response = {{ await_prefix }}getattr(session, method)( | ||
| url, | ||
| timeout=timeout, | ||
| headers=headers, | ||
| params=rest_helpers.flatten_query_params(query_params, strict=True), | ||
| {% if body_spec %} | ||
| data=body, | ||
| {% endif %} | ||
| {% if not is_async and is_streaming_method %} | ||
| stream=True, | ||
| {% endif %} | ||
| ) | ||
| return response | ||
| record_http_response(span, response) | ||
| return response | ||
| {% endmacro %} | ||
|
|
||
| {# rest_call_method_common includes the common code for a rest __call__ method to be | ||
|
|
@@ -240,13 +249,24 @@ def _get_http_options(): | |
| ) | ||
|
|
||
| # Send the request | ||
| response = {{ await_prefix }}{{ async_class_prefix }}{{ service_name }}RestTransport._{{method_name}}._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request{% if body_spec %}, body{% endif %}) | ||
| response = {{ await_prefix }}{{ async_class_prefix }}{{ service_name }}RestTransport._{{method_name}}._get_response( | ||
| self._host, | ||
| metadata, | ||
| query_params, | ||
| self._session, | ||
| timeout, | ||
| transcoded_request, | ||
| {% if body_spec %} | ||
| body, | ||
| {% endif %} | ||
| client_options=getattr(self, "_client_options", None), | ||
| ) | ||
|
|
||
| # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception | ||
| # subclass. | ||
| if response.status_code >= 400: | ||
| {# Note: format_http_response_error takes in more parameters than from_http_response and the | ||
| latter only supports a response of type requests.Response. | ||
| {# Note: format_http_response_error takes in more parameters than from_http_response and the | ||
| latter only supports a response of type requests.Response. | ||
| TODO: Clean up the sync response error handling and use format_http_response_error. | ||
| See issue: https://github.com/googleapis/gapic-generator-python/issues/2116. #} | ||
| {% if is_async %} | ||
|
|
@@ -327,6 +347,10 @@ def _prep_wrapped_messages(self, client_info): | |
| {% endif %} | ||
| default_timeout={{ method.timeout }}, | ||
| client_info=client_info, | ||
| method_name="{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}", | ||
| {% if method.client_streaming or method.server_streaming %} | ||
| is_streaming=True, | ||
| {% endif %} | ||
| ), | ||
| {% endfor %}{# service.methods.values() #} | ||
| {% for method_name in api.mixin_api_methods.keys() %} | ||
|
|
@@ -337,26 +361,12 @@ def _prep_wrapped_messages(self, client_info): | |
| self.{{ method_name|snake_case }}, | ||
| default_timeout=None, | ||
| client_info=client_info, | ||
| method_name="{{ api.mixin_api_signatures[method_name].rpc_name }}", | ||
| ), | ||
| {% endfor %}{# method_name in api.mixin_api_methods.keys() #} | ||
| } | ||
| {% endmacro %} | ||
|
|
||
| {# TODO: This helper logic to check whether `kind` needs to be configured in wrap_method | ||
|
Contributor
Author
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. Comment for Reviewers: This logic got moved to the base transport. |
||
| can be removed once we require the correct version of the google-api-core dependency to | ||
| avoid having a gRPC code path in an async REST call. | ||
| See related issue: https://github.com/googleapis/python-api-core/issues/661. | ||
| In the meantime, if an older version of the dependency is installed (which has a wrap_method with | ||
| no kind parameter), then an async gRPC call will work correctly and async REST transport | ||
| will not be available as a transport. | ||
| See related issue: https://github.com/googleapis/gapic-generator-python/issues/2119. #} | ||
| {% macro wrap_async_method_macro() %} | ||
| def _wrap_method(self, func, *args, **kwargs): | ||
| {# TODO: Remove `pragma: NO COVER` once https://github.com/googleapis/python-api-core/pull/688 is merged. #} | ||
| if self._wrap_with_kind: # pragma: NO COVER | ||
| kwargs["kind"] = self.kind | ||
| return gapic_v1.method_async.wrap_method(func, *args, **kwargs) | ||
| {% endmacro %} | ||
|
|
||
| {# `create_interceptor_class` generates an Interceptor class for | ||
| # synchronous and asynchronous rest transports | ||
|
|
@@ -437,11 +447,11 @@ class {{ async_method_name_prefix }}{{ service.name }}RestInterceptor: | |
|
|
||
| Override in a subclass to read or manipulate the response or metadata after it | ||
| is returned by the {{ service.name }} server but before it is returned to user code. | ||
|
|
||
| We recommend only using this `post_{{ method.name|snake_case }}_with_metadata` | ||
| interceptor in new development instead of the `post_{{ method.name|snake_case }}` interceptor. | ||
| When both interceptors are used, this `post_{{ method.name|snake_case }}_with_metadata` interceptor runs after the | ||
| `post_{{ method.name|snake_case }}` interceptor. The (possibly modified) response returned by | ||
| `post_{{ method.name|snake_case }}` interceptor. The (possibly modified) response returned by | ||
| `post_{{ method.name|snake_case }}` will be passed to | ||
| `post_{{ method.name|snake_case }}_with_metadata`. | ||
| """ | ||
|
|
@@ -487,15 +497,15 @@ class {{ async_method_name_prefix }}{{ service.name }}RestInterceptor: | |
|
|
||
| @property | ||
| def {{ name|snake_case }}(self): | ||
| return self.{{ name|make_private }}(self._session, self._host, self._interceptor) # type: ignore | ||
| return self.{{ name|make_private }}(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore | ||
|
|
||
| class {{ name|make_private }}(_Base{{ service.name }}RestTransport._Base{{name}}, {{ async_method_name_prefix }}{{service.name}}RestStub): | ||
| def __hash__(self): | ||
| return hash("{{ async_method_name_prefix }}{{ service.name }}RestTransport.{{ name }}") | ||
|
|
||
| {% set body_spec = api.mixin_http_options["{}".format(name)][0].body %} | ||
| {{ response_method(body_spec, is_async=is_async, is_streaming_method=None) | indent(4) }} | ||
|
|
||
| {{ async_prefix }}def __call__(self, | ||
| request: {{ sig.request_type }}, *, | ||
| retry: OptionalRetry=gapic_v1.method.DEFAULT, | ||
|
|
@@ -521,7 +531,7 @@ class {{ name|make_private }}(_Base{{ service.name }}RestTransport._Base{{name}} | |
| {% endif %} | ||
| """ | ||
| {{ rest_call_method_common(body_spec, name, service, is_async)|indent(4) }} | ||
|
|
||
| {% if sig.response_type == "None" %} | ||
| return {{ await_prefix }}self._interceptor.post_{{ name|snake_case }}(None) | ||
| {% else %} | ||
|
|
@@ -543,7 +553,7 @@ class {{ name|make_private }}(_Base{{ service.name }}RestTransport._Base{{name}} | |
| "payload": response_payload, | ||
| "headers": dict(response.headers), | ||
| "status": response.status_code, | ||
| } | ||
| } | ||
| _LOGGER.debug( | ||
| "Received response for {{ service.meta.address.proto_package_versioned }}.{{ service.async_client_name }}.{{ name }}", | ||
| extra = { | ||
|
|
@@ -565,7 +575,7 @@ class {{ name|make_private }}(_Base{{ service.name }}RestTransport._Base{{name}} | |
|
|
||
| {% macro client_method_metadata_default_value() %}(){% endmacro %} | ||
|
|
||
| {% macro client_method_metadata_argument_doc() %}metadata ({{ client_method_metadata_type() }}): Key/value pairs which should be | ||
| {% macro client_method_metadata_argument_doc() %}metadata ({{ client_method_metadata_type() }}): Key/value pairs which should be | ||
| sent along with the request as metadata. Normally, each value must be of type `str`, | ||
| but for metadata keys ending with the suffix `-bin`, the corresponding values must | ||
| be of type `bytes`.{% endmacro %} | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
Note
As context for the reviewer:
For native methods (like
EchoorGetSecret), the generator reads the service's.protofile directly, so constructing the name in the template is straightforward:method_name="{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}"However, mixins don't live in the service’s proto. Mixin methods (
GetOperation,GetIamPolicy,ListLocations) are synthetic—they are injected by the generator from the static catalog ingapic/schema/mixins.pyWithout a name attribute, any mixin call (like polling an operation or checking IAM permissions) would be unable to start an OpenTelemetry method span, or would emit an unknown/nameless span that failed our contract checks.