Skip to content

feat(o11y): Introduce rpc.system.name and rpc.method in gRPC#4121

Open
diegomarquezp wants to merge 35 commits intomainfrom
observability/tracing-attr/rpc.method-and-rpc.system
Open

feat(o11y): Introduce rpc.system.name and rpc.method in gRPC#4121
diegomarquezp wants to merge 35 commits intomainfrom
observability/tracing-attr/rpc.method-and-rpc.system

Conversation

@diegomarquezp
Copy link
Contributor

@diegomarquezp diegomarquezp commented Feb 25, 2026

This PR introduces the rpc.system.name and rpc.method span attributes in gRPC.

To achieve this, all Traced*Callable classes were modified with a constructor overload that accepts an ApiTracerContext instance. Such an instance will be preferred over SpanName when deciding which (also newly introduced) overload of ApiTracerFactory.newTracer() will be called.

The new method in ApiTracerFactory, newTracer(ApiTracer parent, ApiTracerContext tracerContext, OperationType operationType) will by default merge the existing factory's context with the new one and simply create a new SpanName.

However, the logic in SpanTracerFactory will use the context to create the span name directly.

The rpc.system.name and rpc.method properties are obtained from GrpcCallableFactory and stored in ApiTracerContext, which now holds the regexes for parsing client and method names (used when building SpanNames).

@product-auto-label product-auto-label bot added the size: m Pull request size is medium. label Feb 25, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @diegomarquezp, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces significant improvements to the observability framework by enriching tracing contexts with RPC system and method information. It refactors the tracing mechanism to be more flexible and less dependent on specific span naming conventions, allowing for better integration with standardized observability practices. The changes streamline how RPC details are captured and propagated through the tracing system, enhancing the clarity and utility of generated traces.

Highlights

  • Enhanced Tracing Context: The ApiTracerContext has been significantly enhanced to include rpc.system.name and rpc.method attributes, along with clientName and methodName. This allows for more granular and standardized observability data in tracing spans.
  • Decoupling Tracing from SpanName: The tracing mechanism no longer directly relies on the SpanName object. Instead, ApiTracerFactory and related callable wrappers now receive an ApiTracerFactory that is pre-configured with the necessary context, including RPC system and method details.
  • Automated RPC Context Extraction: New static factory methods (fromGrpcMethodName and fromHttpJsonMethodName) have been added to ApiTracerContext to automatically parse full method names (gRPC or HTTP/JSON) and extract the rpcSystem, rpcMethod, clientName, and methodName for tracing.
  • Refactored Callable Factories: Both GrpcCallableFactory and HttpJsonCallableFactory have been updated to utilize the new ApiTracerContext for creating tracers, simplifying the tracing setup within these factories and removing redundant logic for extracting method names.
Changelog
  • gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallableFactory.java
    • Removed InternalApi and SpanName related imports.
    • Removed the FULL_METHOD_NAME_REGEX pattern and its usage.
    • Updated various create*Callable methods to use a new getTracerFactory helper method for obtaining ApiTracerFactory instances.
    • Introduced a new private static method getTracerFactory to construct ApiTracerContext from gRPC method descriptors.
  • gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallableFactory.java
    • Removed InternalApi and SpanName related imports.
    • Removed the FULL_METHOD_NAME_REGEX pattern and its usage.
    • Updated createUnaryCallable to use a new getTracerFactory helper method.
    • Introduced a new private static method getTracerFactory to construct ApiTracerContext from HTTP/JSON method descriptors.
  • gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java
    • Added new fields: rpcSystem, rpcMethod, clientName, and methodName.
    • Updated getAttemptAttributes to include rpc.system.name and rpc.method.
    • Added merge method for combining ApiTracerContext instances.
    • Introduced static factory methods fromGrpcMethodName and fromHttpJsonMethodName for parsing method names.
    • Added corresponding builder methods for the new fields.
  • gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java
    • Modified the newTracer method signature to remove the SpanName parameter.
  • gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracerFactory.java
    • Updated the newTracer method signature to align with the ApiTracerFactory interface.
  • gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java
    • Added new constants RPC_SYSTEM_NAME_ATTRIBUTE and RPC_METHOD_ATTRIBUTE.
  • gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracerFactory.java
    • Modified the newTracer method signature to remove the SpanName parameter.
    • Updated the attemptSpanName calculation to use apiTracerContext fields.
    • Modified withContext to merge the provided context with the existing apiTracerContext.
  • gax-java/gax/src/main/java/com/google/api/gax/tracing/TracedUnaryCallable.java
    • Removed the spanName field and its usage.
    • Updated the constructor and futureCall method to no longer accept or use SpanName.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

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

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 a significant and beneficial refactoring to the tracing infrastructure. By replacing the explicit SpanName with a more structured ApiTracerContext carried by ApiTracerFactory, the code becomes more maintainable and extensible. The introduction of rpc.system.name and rpc.method attributes aligns with modern observability practices.

However, there are a couple of points that need attention:

  1. Missing Tests (Critical): The PR adds new logic (e.g., in ApiTracerContext) and refactors existing components, but it lacks corresponding unit tests. The repository's contribution guidelines (line 148: Testing: All new logic should be accompanied by tests.) are not being met. Please add tests for the new functionality, especially for the parsing logic in ApiTracerContext.fromGrpcMethodName and ApiTracerContext.fromHttpJsonMethodName, and ensure existing tests are updated to reflect the API changes.

  2. Robustness: There's a potential for a NullPointerException in SpanTracerFactory which can be addressed with a defensive check.

Overall, this is a good direction, but the lack of tests is a critical issue that must be addressed before merging.

@diegomarquezp diegomarquezp added do not merge Indicates a pull request not ready for merge, due to either quality or timing. needs work This is a pull request that needs a little love. wip Work in Progress labels Feb 25, 2026
Base automatically changed from observability/tracing-attr/repo-artifact to main February 26, 2026 19:03
@diegomarquezp diegomarquezp force-pushed the observability/tracing-attr/rpc.method-and-rpc.system branch from b32f09a to e5b927a Compare February 26, 2026 20:13
@product-auto-label product-auto-label bot added size: l Pull request size is large. and removed size: m Pull request size is medium. labels Feb 27, 2026
@product-auto-label product-auto-label bot added size: xl Pull request size is extra large. and removed size: l Pull request size is large. labels Mar 2, 2026
baseCallable,
clientContext.getTracerFactory(),
getSpanName(grpcCallSettings.getMethodDescriptor()),
getApiTracerContext(grpcCallSettings.getMethodDescriptor()),
Copy link
Contributor

Choose a reason for hiding this comment

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

Batching is technically OOS also, but since this is a very low risk change, I think we can take it.

this.innerCallable = innerCallable;
this.tracerFactory = tracerFactory;
this.spanName = spanName;
this.apiTracerContext = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we create an apiTracerContext from the SpanName so we don't have to do null check below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We would need something like:

    this.apiTracerContext = ApiTracerContext.newBuilder()
            .setLibraryMetadata(LibraryMetadata.empty())
            .setClientName(spanName.getClientName())
            .setMethodName(spanName.getMethodName())
            .build();

However clientName and methodName are computed from fullMethodName (I"m moving the parsing logic to ApiTracerContext.

/**
* @return the client name part of the span name
*/
public String getClientName() {
return getParsedFullMethodNameParts()[0];
}

I think we can achieve this by having a private methodName property that will either return the set value or compute it as it's being done now. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's keep a setter as an override of the default regex parsing logic.

/**
* @return the client name part of the span name
*/
public String getClientName() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is clientName just serviceName which we should already have it in StubSettings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is meant for computing span names, which may differ depending on the transport.

}

/**
* @return the method name part of the span name
Copy link
Contributor

Choose a reason for hiding this comment

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

method name could be used for more than Span, can we use more generic terms instead of span name in the Java docs?

In addition, can we provide an example for each of the public attributes so its easier to understand?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Improved the javadocs, which now just "RPC" instead of "span names".

I added examples to the public attributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

javadocs kept but I moved the SpanName related logic to SpanName instead of ApiTracerContext.

*/
default ApiTracer newTracer(
ApiTracer parent, ApiTracerContext tracerContext, OperationType operationType) {
SpanName spanName = SpanName.of(tracerContext.getClientName(), tracerContext.getMethodName());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pass full method name and transport; let span name do the parsing (keep the regexes in there).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Making getters such as fullMethodName() package-private makes GrpcCallableFactory and HttpJsonCallableFactory fail due to lack of access.

Passing the whole context solves the problem.

@diegomarquezp diegomarquezp requested a review from blakeli0 March 4, 2026 16:29
@diegomarquezp
Copy link
Contributor Author

/gemini review

Copy link
Contributor

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

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 rpc.system.name and rpc.method span attributes for gRPC tracing, which is a great enhancement for observability. The implementation is well-structured, refactoring tracing context creation into a new ApiTracerContext class and adding new overloads to the Traced*Callable classes and ApiTracerFactory. The test coverage is excellent, with new parameterized tests to handle both old and new code paths, and an integration test to verify the new attributes are correctly added to spans. I have a couple of minor suggestions to improve documentation and code consistency.

diegomarquezp and others added 3 commits March 4, 2026 12:26
…ilityAttributes.java

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
* @return a new {@link SpanName} with the parsed client name and method name
*/
public static SpanName of(ApiTracerContext apiTracerContext) {
Preconditions.checkState(apiTracerContext.fullMethodName() != null, "rpcMethod must be set");
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think fullMethodName() is nullable? Also can we only pass a String to this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think fullMethodName() is nullable?

I made it @Nullable considering the client-level vs method-def-level contexts we can have. ClientContext will not populate fullMethodName().

Also can we only pass a String to this method?

The problem with SpanName.of(apiTracerContext.getFullMethodName()) is that the callable factories cannot access the package-private method.

@InternalApi("Visible for testing")
static SpanName getSpanName(@Nonnull ApiMethodDescriptor<?, ?> methodDescriptor) {
ApiTracerContext apiTracerContext =
ApiTracerContext.newBuilder()
.setFullMethodName(methodDescriptor.getFullMethodName())
.setTransport(ApiTracerContext.Transport.HTTP)
.setLibraryMetadata(LibraryMetadata.empty())
.build();
return SpanName.of(apiTracerContext);
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I made it @nullable considering the client-level vs method-def-level contexts we can have. ClientContext will not populate fullMethodName()

I see, makes sense. This might be another data point that it's time to create a ClientTracerContext that only contains client level tracer info. And we can merge it with method/request level before we create a request level tracer. I feel we have a lot of nullable fields in ApiTracerContext only because we don't have the info initially. But we can do it in another PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, I'll follow up with separating the contexts.

this.innerCallable = innerCallable;
this.tracerFactory = tracerFactory;
this.apiTracerContext = apiTracerContext;
this.spanName = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

We can create a spanName from ApiTracerContext now.

Copy link
Contributor Author

@diegomarquezp diegomarquezp Mar 4, 2026

Choose a reason for hiding this comment

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

But we wouldn't use it if we have the context, right?

AFAIU we are trying to provide logic that only handles a context object as the SoT for SpanName. That's why we made this change in ApiTracer:

if (apiTracerContext != null) {
tracer = tracerFactory.newTracer(context.getTracer(), apiTracerContext, OperationType.Unary);
} else {
tracer = tracerFactory.newTracer(context.getTracer(), spanName, OperationType.Unary);
}

default ApiTracer newTracer(
ApiTracer parent, ApiTracerContext tracerContext, OperationType operationType) {
SpanName spanName = SpanName.of(tracerContext);
return newTracer(parent, spanName, operationType);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed. Even if we don't use it, I think it's safer to populate it instead of making it nullable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, done.

private final UnaryCallable<RequestT, ResponseT> innerCallable;
private final ApiTracerFactory tracerFactory;
private final SpanName spanName;
@Nullable private final SpanName spanName;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can keep this spanName not nullable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll do it once we confirm my question in #4121 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

See answers below. Ideally we probably want to remove SpanName as a field, and only use ApiTracerContext as the source of truth. But it can not be done until we migrated all Callables to use ApiTracerContext.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

until we migrated all Callables to use ApiTracerContext

I thought the problem was with other classes that create the callables, such as GrpcCallableFactory. Are there usages of Traced*Callable in downstream libraries?

* @param tracerContext the method-definition-specific tracer context
* @param operationType the type of operation that the tracer will trace
*/
default ApiTracer newTracer(
Copy link
Contributor

Choose a reason for hiding this comment

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

The downstream failure in bigtable is legit. One possible solution is to move OperationType to ApiTracerContext

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 5, 2026

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 5, 2026

Quality Gate Failed Quality Gate failed for 'java_showcase_integration_tests'

Failed conditions
78.1% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

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

Labels

size: xl Pull request size is extra large.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants