Add GraphQL 20 local-root span enrichment and operation-span suppression#19177
Add GraphQL 20 local-root span enrichment and operation-span suppression#19177amccague wants to merge 3 commits into
Conversation
The graphql-java-20.0 operation span is SpanKind.INTERNAL and advertises no span key. SEMCONV span suppression therefore never applies to it, so it cannot be deduplicated against an enclosing span the way server spans are. This adds three independent, opt-in options (all defaults preserve current behaviour) so the GraphQL telemetry can instead, or additionally, land on the enclosing span, emulating the effect of span suppression explicitly: - setOperationSpanEnabled (default true): when false, no `GraphQL Operation` span is created. Data fetcher spans, if enabled, still nest under the enclosing span. If this is off and local-root enrichment is also off, no operation-level telemetry is produced (permitted, not validated). - setAddAttributesToLocalRootSpan (default false): writes graphql.operation.*, graphql.document, and exception events onto the local root span (typically the HTTP server span), via LocalRootSpan.fromContextOrNull. No-ops when there is no enclosing local root. - setPromoteErrorStatusToLocalRootSpan (default false): sets the local root span status to ERROR on any GraphQL error. Only ever sets ERROR, never clears an existing status; independent of the attribute toggle. Deliberate decisions: - The local root span is never renamed, only enriched. - Error status is NOT promoted by default: GraphQL errors are often partial or expected and are not HTTP failures, so marking a 200 response's server span ERROR must be explicitly opted in. Enriching the local root span follows existing patterns in this repo, e.g. spring-security UserAttributesCapturer and spring-cloud-gateway ServerWebExchangeHelper.
1b8b344 to
a0b4f6c
Compare
There was a problem hiding this comment.
Pull request overview
Adds opt-in GraphQL 20 telemetry enrichment on local-root spans and optional operation-span suppression.
Changes:
- Adds three library and Javaagent configuration options.
- Propagates GraphQL attributes, errors, and optional error status to local-root spans.
- Adds documentation and behavioral tests.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
instrumentation/graphql-java/README.md |
Documents configuration keys. |
.../OpenTelemetryInstrumentationState.java |
Tracks local-root and operation-span state. |
.../OpenTelemetryInstrumentationHelper.java |
Implements suppression and enrichment. |
.../GraphqlAttributesExtractor.java |
Shares GraphQL attribute keys. |
.../metadata.yaml |
Registers new configuration options. |
.../GraphqlLocalRootSpanTest.java |
Tests local-root behavior. |
.../OpenTelemetryInstrumentation.java |
Reuses shared error-event handling. |
.../GraphQLTelemetryBuilder.java |
Exposes new builder options. |
.../GraphQLTelemetry.java |
Passes options into instrumentation. |
.../GraphqlInstrumenterFactory.java |
Forwards configuration to the helper. |
.../library/README.md |
Documents library builder options. |
.../GraphqlSingletons.java |
Wires Javaagent configuration. |
| state.setOperationName(operationName); | ||
|
|
||
| String query = null; | ||
| if (captureQuery) { |
There was a problem hiding this comment.
This is confined to configuration where operationSpanEnabled=false and addAttributesToLocalRootSpan=false i.e. no telemetry is produced at all. That combination is intentionally permitted but, as documented on setOperationSpanEnabled, is equivalent to not installing the instrumentation.
The performance cost is negligible.
Pull request dashboard statusStatus last refreshed: 2026-07-27 01:50:32 UTC.
This automated status or its linked feedback items may be incorrect. If something looks wrong, please report it with the result you expected. If you believe this pull request is incorrectly routed as waiting on the author, comment |
…es absent Add a test asserting that when the operation span is disabled, data fetcher spans nest directly under the enclosing server span. Also assert graphql.operation.name (not just type and document) is absent when only error status promotion is enabled.
| .setOperationSpanEnabled(config.operationSpanEnabled) | ||
| .setAddAttributesToLocalRootSpan(config.addAttributesToLocalRootSpan) | ||
| .setPromoteErrorStatusToLocalRootSpan(config.promoteErrorStatusToLocalRootSpan) |
There was a problem hiding this comment.
The behaviour driven by these options is fully covered by the library tests, which exercise the builder settings directly. I'm not sure how valuable this duplication is?
Verify the sanitized document value is copied onto the local root span in enrichAndOwnSpan, not just operation name and type.
| static final AttributeKey<String> GRAPHQL_DOCUMENT = AttributeKey.stringKey("graphql.document"); | ||
| static final AttributeKey<String> GRAPHQL_OPERATION_NAME = | ||
| AttributeKey.stringKey("graphql.operation.name"); | ||
| private static final AttributeKey<String> GRAPHQL_OPERATION_TYPE = | ||
| static final AttributeKey<String> GRAPHQL_OPERATION_TYPE = | ||
| AttributeKey.stringKey("graphql.operation.type"); |
There was a problem hiding this comment.
The private static final in the style guide is shown as an example, not stated as a requirement, and no check enforces the modifier. Widening to package-private keeps a single copy shared by the extractor and the helper in the same module, avoiding a duplicated set of keys that could drift.
Happy to switch to duplicated private copies if preferred.
| // Only rename our own operation span; never rename the local root (e.g. the server span). | ||
| // Guarded on whether we actually created the span, not merely on the config flag, so that a | ||
| // suppressed span (state context == parent context) is never renamed. | ||
| if (state.isOperationSpanCreated()) { |
There was a problem hiding this comment.
Every local-root enrichment test asserts the enclosing server span keeps its name (GET), and enrichWithoutOwnSpan exercises the operation-span-disabled path specifically.
The invariant is already covered.
| /** | ||
| * The local root span captured from the parent context, used when enriching it with GraphQL | ||
| * telemetry. {@code null} when there is no enclosing local root or when local-root enrichment is | ||
| * disabled. | ||
| */ |
There was a problem hiding this comment.
It would be good if we can avoid "local root span" because it's not a well-defined concept in the OpenTelemetry Specification or Semantic Conventions.
could we reframe this PR around some set of these instead?
- create span and stamp attributes on that span, or
- don't create span and stamp attributes on to the current span, or
- maybe some hybrid where stamp on to current span if valid, otherwise create span
The motivation here was to make GraphQL attributes available on the server span, since some OTEL backends work best when this main application span is as rich as possible rather than needing to coalesce or join them together.
This adds three independent, opt-in options (all defaults preserve current behaviour) so the GraphQL telemetry can instead, or additionally, land on the enclosing span, emulating the effect of span suppression explicitly:
setOperationSpanEnabled(defaulttrue): whenfalse, noGraphQL Operationspan is created. Data fetcher spans, if enabled, still nest under the enclosing span. If this is off and local-root enrichment is also off, no operation-level telemetry is produced (permitted, not validated).setAddAttributesToLocalRootSpan(defaultfalse): writesgraphql.operation.*,graphql.document, and exception events onto the local root span (typically the HTTP server span). No-ops when there is no enclosing local root.setPromoteErrorStatusToLocalRootSpan(defaultfalse): sets the local root span status toERRORon any GraphQL error. Only ever setsERROR, never clears an existing status; independent of the attribute toggle.Deliberate decisions:
ERRORmust be explicitly opted in.Enriching the local root span follows existing patterns in this repo, e.g. spring-security UserAttributesCapturer and spring-cloud-gateway ServerWebExchangeHelper.