Skip to content

feat: add observability on subgraph connections and resolvers - #2881

Merged
alepane21 merged 52 commits into
mainfrom
ale/eng-9608-dns-lookup-timeout-impacting-wundergraph-router-latency
Jul 3, 2026
Merged

feat: add observability on subgraph connections and resolvers#2881
alepane21 merged 52 commits into
mainfrom
ale/eng-9608-dns-lookup-timeout-impacting-wundergraph-router-latency

Conversation

@alepane21

@alepane21 alepane21 commented May 21, 2026

Copy link
Copy Markdown
Contributor

This PR add additional metrics on connection acquisition:

  • router.http.client.dns_lookup_duration
  • router.http.client.tcp_connect_duration
  • router.http.client.tls_handshake_duration
  • router.http.client.time_to_first_byte

Also expose resolvers usage:

  • router.engine.resolvers.max_concurrent
  • router.engine.resolvers.inflight

And finally operation variables:

  • request.operation.variables

This new variables are also available in the access logs or subgraph access logs, depending on where it makes sense. On the subgraph access logs we also added access to subgraph response headers.

Summary by CodeRabbit

  • New Features

    • Enhanced connection telemetry: per-request DNS, TCP connect, TLS handshake and time-to-first-byte timing metrics and optional per-request connection-phase spans; resolver-acquire duration metric and optional resolver-acquire spans.
    • Resolver concurrency metrics: max-concurrent and in-flight counts.
  • Configuration

    • New toggles to enable enhanced connection stats and resolver engine stats for tracing and metrics (OTLP & Prometheus).
  • Tests

    • Added/updated telemetry tests validating resolver metrics, enhanced HTTP client trace metrics and connection-phase/resolver spans.

Review Change Stack

Checklist

Open Source AI Manifesto

This project follows the principles of the Open Source AI Manifesto. Please ensure your contribution aligns with its principles.

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds resolver concurrency metrics and enhanced HTTP client-phase metrics (DNS, TCP, TLS, TTFB). Introduces config flags and schema updates, captures per-phase timings via httptrace, registers instruments in OTLP/Prometheus/no-op providers, wires MetricStore into GraphQL handler/websocket flows, registers executor resolvers with EngineStats, emits phase and fetch-processing spans, and updates tests to validate metrics and spans.

Changes

Resolver Concurrency and Enhanced Connection Metrics

Layer / File(s) Summary
Configuration and schema
router/pkg/config/*, router/pkg/metric/config.go, router/pkg/config/config.schema.json, router/pkg/config/fixtures/*, router/pkg/config/testdata/*
Adds enhanced_connection_stats and engine_stats.resolvers flags to tracing and metrics config; maps these flags into metric and trace option structs.
Engine statistics and resolver reporting
router/pkg/statistics/engine_stats.go
Adds ResolverConcurrencyReporter, RegisterResolver/UnregisterResolver, initializes resolver reporter registry, extends UsageReport with resolver fields, and aggregates resolver metrics in GetReport.
HTTP trace phase capture & retro spans
router/internal/traceclient/traceclient.go, router/core/trace_dialer.go
Records DNS/TCP/TLS/write/first-byte timestamps via httptrace callbacks, refactors metric processing, emits per-phase metrics, and optionally creates retroactive per-phase child spans.
Enhanced connection instruments & stores
router/pkg/metric/connection_measurements.go, router/pkg/metric/connection_metric_store.go, router/pkg/metric/oltp_connection_metric_store.go, router/pkg/metric/prom_connection_metric_store.go, router/pkg/metric/noop_connection_metrics.go
Adds enhanced per-phase histogram instruments, extends provider/store interfaces with DNS/TCP/TLS/TTFB measurement methods, and conditionally creates instruments when enabled.
Resolver acquire duration metrics
router/pkg/metric/metric_store.go, router/pkg/metric/measurements.go, router/pkg/metric/otlp_metric_store.go, router/pkg/metric/prom_metric_store.go, router/pkg/metric/noop_metrics.go
Adds ResolverAcquireDurationHistogram, registers it when enabled, and implements MeasureResolverAcquireDuration for OTLP, Prometheus, and noop providers.
Engine metrics for resolvers
router/pkg/metric/engine_metrics.go
Adds observable instruments resolvers.max_concurrent and resolvers.inflight, includes them in instrument lists, and observes resolver values from UsageReport.
Core wiring: handler, websocket, transport, loader hooks
router/core/graphql_handler.go, router/core/websocket.go, router/core/graph_server.go, router/core/transport.go, router/core/engine_loader_hooks.go, router/internal/context/*, router/pkg/trace/transport.go
Wires MetricStore into GraphQL handler and websocket sync paths to record resolver acquire durations and optionally emit resolver-acquire spans; registers/unregisters executor resolver with EngineStats; passes tracerProvider and emit-phase flags into transport/traceclient; attaches FetchTraceTimings and emits fetch-processing and response-body read spans when enabled.
Test environment and validation
router-tests/testenv/*, router-tests/observability/prometheus_test.go, router-tests/telemetry/connection_phase_spans_test.go, router-tests/*go.mod, router/go.mod
SyncReporter registers resolver reporters and aggregates their metrics; testenv exposes enhanced-connection and resolver toggles; Prometheus tests updated to expect resolver metric families and enhanced connection histograms; new tracing tests validate phase spans and resolver-acquire spans; test dependencies updated.

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the main change: adding observability for subgraph connections and resolvers, which directly aligns with the substantial changes across metrics, tracing, configuration, and test files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 62.13018% with 128 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.42%. Comparing base (3c22488) to head (63685af).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
router/internal/traceclient/traceclient.go 58.33% 27 Missing and 3 partials ⚠️
router/pkg/statistics/engine_stats.go 8.33% 22 Missing ⚠️
router/pkg/metric/oltp_connection_metric_store.go 25.00% 10 Missing and 2 partials ⚠️
router/pkg/metric/metric_store.go 26.66% 11 Missing ⚠️
...r/internal/expr/use_request_operation_variables.go 67.74% 7 Missing and 3 partials ⚠️
router/pkg/metric/connection_measurements.go 38.46% 4 Missing and 4 partials ⚠️
router/pkg/metric/connection_metric_store.go 68.00% 8 Missing ⚠️
router/pkg/metric/prom_connection_metric_store.go 50.00% 8 Missing ⚠️
router/pkg/metric/engine_metrics.go 88.88% 2 Missing and 2 partials ⚠️
router/pkg/metric/noop_connection_metrics.go 50.00% 4 Missing ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2881      +/-   ##
==========================================
- Coverage   61.45%   61.42%   -0.04%     
==========================================
  Files         260      261       +1     
  Lines       30282    30563     +281     
==========================================
+ Hits        18609    18772     +163     
- Misses      10176    10280     +104     
- Partials     1497     1511      +14     
Files with missing lines Coverage Δ
router/core/engine_loader_hooks.go 91.17% <100.00%> (+0.10%) ⬆️
router/core/graph_server.go 85.38% <100.00%> (+0.11%) ⬆️
router/core/graphql_handler.go 61.91% <100.00%> (+0.86%) ⬆️
router/core/graphql_prehandler.go 85.61% <100.00%> (+0.03%) ⬆️
router/core/request_context_fields.go 79.24% <100.00%> (+2.03%) ⬆️
router/core/router_config.go 93.97% <100.00%> (+0.14%) ⬆️
router/core/trace_dialer.go 58.97% <ø> (ø)
router/core/websocket.go 77.33% <100.00%> (+0.30%) ⬆️
router/internal/expr/expr.go 95.74% <100.00%> (ø)
router/internal/expr/expr_manager.go 95.55% <ø> (ø)
... and 19 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@router/core/graph_server.go`:
- Around line 1463-1467: The resolver is being registered with s.engineStats
before buildGraphMux has finished and error paths can leave it registered; move
the s.engineStats.RegisterResolver(executor.Resolver) call (and its
corresponding context.AfterFunc/unregister logic) so it runs only after
buildGraphMux completes without error, or alternatively add a cleanup path that
calls s.engineStats.UnregisterResolver(executor.Resolver) on any early returns;
update the code around buildGraphMux/graphMuxCtx/context.AfterFunc to ensure
executor.Resolver is only registered when the mux cannot fail and that
UnregisterResolver is always invoked on error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 311e666b-9a5a-40d5-ab26-6246279c9b08

📥 Commits

Reviewing files that changed from the base of the PR and between fd5ceed and e80f7aa.

📒 Files selected for processing (27)
  • router-tests/observability/prometheus_test.go
  • router-tests/testenv/sync_reporter.go
  • router-tests/testenv/testenv.go
  • router/core/graph_server.go
  • router/core/graphql_handler.go
  • router/core/router.go
  • router/core/trace_dialer.go
  • router/core/websocket.go
  • router/internal/traceclient/traceclient.go
  • router/pkg/config/config.go
  • router/pkg/config/config.schema.json
  • router/pkg/config/fixtures/full.yaml
  • router/pkg/config/testdata/config_defaults.json
  • router/pkg/config/testdata/config_full.json
  • router/pkg/metric/config.go
  • router/pkg/metric/connection_measurements.go
  • router/pkg/metric/connection_metric_store.go
  • router/pkg/metric/engine_metrics.go
  • router/pkg/metric/measurements.go
  • router/pkg/metric/metric_store.go
  • router/pkg/metric/noop_connection_metrics.go
  • router/pkg/metric/noop_metrics.go
  • router/pkg/metric/oltp_connection_metric_store.go
  • router/pkg/metric/otlp_metric_store.go
  • router/pkg/metric/prom_connection_metric_store.go
  • router/pkg/metric/prom_metric_store.go
  • router/pkg/statistics/engine_stats.go

Comment thread router/core/graph_server.go
@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown

Router-nonroot image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-0a1e2744f2cc704a03d60e1bd1eccf93f19bc685-nonroot

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

🧹 Nitpick comments (1)
router/core/transport.go (1)

202-257: ⚡ Quick win

Guard the tracer provider before boxing it into the interface.

t.tracerProvider is a *sdktrace.TracerProvider, but NewCustomTransport now takes otrace.TracerProvider. A nil pointer boxed into that interface is non-nil, so the downstream opts.TracerProvider != nil check can succeed and call Tracer(...) on a nil receiver.

♻️ Suggested fix
 func (t TransportFactory) RoundTripper(baseTransport http.RoundTripper) http.RoundTripper {
 	if t.localhostFallbackInsideDocker && docker.Inside() {
 		baseTransport = docker.NewLocalhostFallbackRoundTripper(baseTransport)
 	}
@@
 	traceTransport := trace.NewTransport(
 		baseTransport,
 		otelHttpOptions,
 		trace.WithPreHandler(func(r *http.Request) {
@@
 		}),
 	)
+
+	var tracerProvider otrace.TracerProvider
+	if t.tracerProvider != nil {
+		tracerProvider = t.tracerProvider
+	}
+
 	tp := NewCustomTransport(
 		traceTransport,
 		t.retryOptions,
 		t.metricStore,
 		t.connectionMetricStore,
 		t.circuitBreaker,
 		t.enableTraceClient,
-		t.tracerProvider,
+		tracerProvider,
 		t.emitConnectionPhaseSpan,
 	)

Also applies to: 307-315

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/core/transport.go` around lines 202 - 257, NewTransport is assigning
opts.TracerProvider (a concrete *sdktrace.TracerProvider) directly into the
TransportFactory.tracerProvider interface, which can box a nil pointer as
non-nil; guard the assignment by checking if opts.TracerProvider != nil and only
then set TransportFactory.tracerProvider = opts.TracerProvider, otherwise set it
to a nil interface value (e.g. leave it nil or explicitly cast
otrace.TracerProvider(nil)); apply the same nil-check pattern in the other
constructor (NewCustomTransport / the block around lines 307-315) that assigns
TracerProvider from TransportOptions so downstream calls to Tracer(...) never
operate on a boxed nil receiver.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@router/core/transport.go`:
- Around line 202-257: NewTransport is assigning opts.TracerProvider (a concrete
*sdktrace.TracerProvider) directly into the TransportFactory.tracerProvider
interface, which can box a nil pointer as non-nil; guard the assignment by
checking if opts.TracerProvider != nil and only then set
TransportFactory.tracerProvider = opts.TracerProvider, otherwise set it to a nil
interface value (e.g. leave it nil or explicitly cast
otrace.TracerProvider(nil)); apply the same nil-check pattern in the other
constructor (NewCustomTransport / the block around lines 307-315) that assigns
TracerProvider from TransportOptions so downstream calls to Tracer(...) never
operate on a boxed nil receiver.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 64974d72-3fda-4020-ace7-11b31f169b56

📥 Commits

Reviewing files that changed from the base of the PR and between 6620419 and 1fcb928.

📒 Files selected for processing (14)
  • router-tests/telemetry/connection_phase_spans_test.go
  • router-tests/testenv/testenv.go
  • router/core/graph_server.go
  • router/core/graphql_handler.go
  • router/core/router.go
  • router/core/transport.go
  • router/core/websocket.go
  • router/internal/traceclient/traceclient.go
  • router/pkg/config/config.go
  • router/pkg/config/config.schema.json
  • router/pkg/config/fixtures/full.yaml
  • router/pkg/config/testdata/config_defaults.json
  • router/pkg/config/testdata/config_full.json
  • router/pkg/trace/config.go

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
router-tests/telemetry/connection_phase_spans_test.go (1)

61-133: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wait for the expected spans before asserting on them.

Both enabled-path subtests read exporter.GetSpans().Snapshots() exactly once right after the request. That makes the assertions on Resolver - Acquire, response-body, and response-processing spans timing-sensitive. Wrap the snapshot/assertion block in require.Eventually and wait until all expected span names are present before checking parentage and durations.

As per coding guidelines: For periodic exporters, wait for ALL expected items using require.Eventually, not just one sentinel value, to avoid race conditions with export cycles.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router-tests/telemetry/connection_phase_spans_test.go` around lines 61 - 133,
The test reads exporter.GetSpans().Snapshots() once and asserts on
timing-sensitive spans; wrap the snapshot + assertion logic in a
require.Eventually to wait until all expected spans are present before checking
parentage/durations. Specifically, in the blocks using
exporter.GetSpans().Snapshots(), call require.Eventually and inside its closure
repeatedly call exporter.GetSpans().Snapshots() and assert presence of names via
hasSpanWithName / spansByName (e.g. "HTTP - TCP Connect", "HTTP - Time To First
Byte", "HTTP - Read Response Body", "Engine - Fetch Response Processing", and
"Resolver - Acquire"), then only after the Eventually returns proceed to use
indexByID, spansByName and the duration/parent assertions; pick a reasonable
timeout/tick (e.g. a few seconds) so the periodic exporter can flush.
♻️ Duplicate comments (1)
router/core/graph_server.go (1)

1465-1469: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Register the resolver only after the remaining setup can no longer fail.

buildGraphMux still has several error returns after Line 1465. On any of those paths, graphMuxCtx is never canceled, so this AfterFunc never runs and the resolver stays registered in s.engineStats, leaving stale resolver-concurrency metrics behind on failed reloads/tests. Move this block to the last no-fail section before success, or explicitly unregister on every later error path.

Based on learnings: ensure that buildGraphMux error paths clean up partially initialized resources (caches, metric stores, pub/sub providers, connectors) before returning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/core/graph_server.go` around lines 1465 - 1469, The resolver is
registered with s.engineStats (via
s.engineStats.RegisterResolver(executor.Resolver)) before buildGraphMux finishes
and later error returns can leave it registered; move the RegisterResolver +
context.AfterFunc(UnregisterResolver) pair to the final no-fail section of
buildGraphMux (after all remaining initialization succeeds) so registration
happens only on success, or alternatively ensure every subsequent error return
path explicitly calls s.engineStats.UnregisterResolver(executor.Resolver) (or
cancels graphMuxCtx) to clean up; locate the current calls to
s.engineStats.RegisterResolver and s.engineStats.UnregisterResolver and adjust
them around buildGraphMux, graphMuxCtx, and executor.Resolver accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@router-tests/telemetry/connection_phase_spans_test.go`:
- Around line 61-133: The test reads exporter.GetSpans().Snapshots() once and
asserts on timing-sensitive spans; wrap the snapshot + assertion logic in a
require.Eventually to wait until all expected spans are present before checking
parentage/durations. Specifically, in the blocks using
exporter.GetSpans().Snapshots(), call require.Eventually and inside its closure
repeatedly call exporter.GetSpans().Snapshots() and assert presence of names via
hasSpanWithName / spansByName (e.g. "HTTP - TCP Connect", "HTTP - Time To First
Byte", "HTTP - Read Response Body", "Engine - Fetch Response Processing", and
"Resolver - Acquire"), then only after the Eventually returns proceed to use
indexByID, spansByName and the duration/parent assertions; pick a reasonable
timeout/tick (e.g. a few seconds) so the periodic exporter can flush.

---

Duplicate comments:
In `@router/core/graph_server.go`:
- Around line 1465-1469: The resolver is registered with s.engineStats (via
s.engineStats.RegisterResolver(executor.Resolver)) before buildGraphMux finishes
and later error returns can leave it registered; move the RegisterResolver +
context.AfterFunc(UnregisterResolver) pair to the final no-fail section of
buildGraphMux (after all remaining initialization succeeds) so registration
happens only on success, or alternatively ensure every subsequent error return
path explicitly calls s.engineStats.UnregisterResolver(executor.Resolver) (or
cancels graphMuxCtx) to clean up; locate the current calls to
s.engineStats.RegisterResolver and s.engineStats.UnregisterResolver and adjust
them around buildGraphMux, graphMuxCtx, and executor.Resolver accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 18be247e-ae39-4b8d-94f4-c632d5f4d407

📥 Commits

Reviewing files that changed from the base of the PR and between aa1010a and 0bafa37.

⛔ Files ignored due to path filters (2)
  • router-tests/go.sum is excluded by !**/*.sum
  • router/go.sum is excluded by !**/*.sum
📒 Files selected for processing (13)
  • router-tests/go.mod
  • router-tests/telemetry/connection_phase_spans_test.go
  • router/core/engine_loader_hooks.go
  • router/core/engine_loader_hooks_test.go
  • router/core/graph_server.go
  • router/core/graphql_handler.go
  • router/core/transport.go
  • router/core/websocket.go
  • router/go.mod
  • router/internal/context/fetch_trace_timings.go
  • router/internal/context/keys.go
  • router/pkg/trace/transport.go
  • router/pkg/trace/transport_test.go
✅ Files skipped from review due to trivial changes (1)
  • router/internal/context/fetch_trace_timings.go

@alepane21
alepane21 marked this pull request as ready for review June 5, 2026 09:05
@alepane21
alepane21 requested a review from a team as a code owner June 5, 2026 09:05

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

…up-timeout-impacting-wundergraph-router-latency
…up-timeout-impacting-wundergraph-router-latency
@alepane21
alepane21 requested a review from a team as a code owner June 12, 2026 14:41
…up-timeout-impacting-wundergraph-router-latency

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

lgtm besides what I commented

Comment thread docs-website/router/configuration/template-expressions.mdx Outdated
Comment thread router/internal/expr/expr.go Outdated
@alepane21
alepane21 merged commit 4169176 into main Jul 3, 2026
39 checks passed
@alepane21
alepane21 deleted the ale/eng-9608-dns-lookup-timeout-impacting-wundergraph-router-latency branch July 3, 2026 10:43
@coderabbitai coderabbitai Bot mentioned this pull request Jul 14, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants