Skip to content

Add GraphQL 20 local-root span enrichment and operation-span suppression#19177

Open
amccague wants to merge 3 commits into
open-telemetry:mainfrom
amccague:feature/graphql-local-root-attributes
Open

Add GraphQL 20 local-root span enrichment and operation-span suppression#19177
amccague wants to merge 3 commits into
open-telemetry:mainfrom
amccague:feature/graphql-local-root-attributes

Conversation

@amccague

@amccague amccague commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

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 (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). 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.

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.
@amccague
amccague force-pushed the feature/graphql-local-root-attributes branch from 1b8b344 to a0b4f6c Compare July 10, 2026 15:33
@amccague
amccague marked this pull request as ready for review July 10, 2026 20:08
@amccague
amccague requested a review from a team as a code owner July 10, 2026 20:08
Copilot AI review requested due to automatic review settings July 10, 2026 20:08

Copilot AI 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.

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) {

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 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.

@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Jul 10, 2026

Copy link
Copy Markdown

Pull request dashboard status

Status last refreshed: 2026-07-27 01:50:32 UTC.

  • Waiting on: Author
  • Next step: Address or respond to 1 review feedback item:
    • Inline threads: 1
    • For each item, reply to move the discussion forward, e.g. link to the commit that addresses it, explain why no change is needed, or ask a follow-up question.

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 /dashboard route:reviewers to route it from waiting on the author to waiting on reviewers. If the last refreshed time above predates your latest reply or push, the dashboard hasn't processed it yet.

…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.

Copilot AI 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.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Comment on lines +34 to +36
.setOperationSpanEnabled(config.operationSpanEnabled)
.setAddAttributesToLocalRootSpan(config.addAttributesToLocalRootSpan)
.setPromoteErrorStatusToLocalRootSpan(config.promoteErrorStatusToLocalRootSpan)

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.

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.

Copilot AI 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.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Comment on lines +24 to 28
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");

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.

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.

Comment on lines +208 to +211
// 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()) {

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.

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.

Comment on lines +44 to +48
/**
* 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.
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

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.

3 participants