Skip to content

feat(router): add metric request timetolastbyte - #3119

Open
alepane21 wants to merge 6 commits into
mainfrom
ale/router-150-router-add-metric-request-timetolastbyte
Open

feat(router): add metric request timetolastbyte#3119
alepane21 wants to merge 6 commits into
mainfrom
ale/router-150-router-add-metric-request-timetolastbyte

Conversation

@alepane21

@alepane21 alepane21 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added network timing metrics for time to last request byte and time to last response byte.
    • Added client-trace fields for accessing these timings in template expressions and structured logs.
    • Metrics now account for complete response-body consumption and applicable request phases.
  • Documentation

    • Expanded OTLP and Prometheus references with definitions, recording conditions, and request deduplication behavior.
  • Tests

    • Added coverage for metrics and logging output, including streaming, errors, early closes, and no-body responses.

Checklist

Open Source AI Manifesto

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

@mintlify

mintlify Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
wundergraphinc 🟢 Ready View Preview Jul 27, 2026, 5:08 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7333b01a-ce67-4ed7-bfa2-c2097780c1e7

📥 Commits

Reviewing files that changed from the base of the PR and between 07a33ad and 2b9ebc1.

📒 Files selected for processing (3)
  • router/internal/traceclient/traceclient.go
  • router/internal/traceclient/traceclient_test.go
  • router/pkg/metric/connection_metric_store.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • router/internal/traceclient/traceclient_test.go

Walkthrough

Adds time-to-last-request-byte and time-to-last-byte tracing, metrics, expression fields, tests, schemas, and documentation. Measurements coordinate asynchronous request callbacks with response-body completion and exclude unsupported response scenarios.

Changes

Last-byte network observability

Layer / File(s) Summary
Metric contracts and instruments
router/internal/expr/expr.go, router/pkg/metric/*, router/pkg/config/config.schema.json
Adds last-byte expression fields, metric interfaces, OTLP and Prometheus instruments, provider methods, no-op implementations, and schema descriptions.
HTTP trace and body completion recording
router/internal/traceclient/traceclient.go
Tracks request completion and response last-byte timing across retries, late callbacks, body consumption, content length, and excluded response types.
Traceclient timing validation
router/internal/traceclient/traceclient_test.go
Adds coverage for body completion, errors, early close, retries, no-body responses, upgraded bodies, streaming responses, and asynchronous callbacks.
Telemetry coverage and documentation
router-tests/observability/*, router-tests/telemetry/connection_metrics_test.go, docs-website/router/*
Validates Prometheus, OTLP, and structured-log outputs and documents the new metrics, expressions, emission conditions, and deduplication behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title identifies the router metric addition and matches the main change, although its wording is compressed.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

Router image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-197711ade61d4d6e64e1bffd582ab14e89ce3e0d

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.74576% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.50%. Comparing base (64eaf60) to head (2b9ebc1).
⚠️ Report is 24 commits behind head on main.

Files with missing lines Patch % Lines
router/internal/traceclient/traceclient.go 84.56% 15 Missing and 8 partials ⚠️
router/pkg/metric/connection_measurements.go 0.00% 2 Missing and 2 partials ⚠️

❌ Your patch check has failed because the patch coverage (84.74%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3119      +/-   ##
==========================================
+ Coverage   62.37%   62.50%   +0.13%     
==========================================
  Files         262      262              
  Lines       31003    31168     +165     
==========================================
+ Hits        19337    19483     +146     
- Misses      10158    10170      +12     
- Partials     1508     1515       +7     
Files with missing lines Coverage Δ
router/internal/expr/expr.go 95.74% <ø> (ø)
router/pkg/metric/connection_metric_store.go 75.75% <100.00%> (+3.34%) ⬆️
router/pkg/metric/noop_connection_metrics.go 65.21% <100.00%> (+7.32%) ⬆️
router/pkg/metric/oltp_connection_metric_store.go 75.75% <100.00%> (+12.42%) ⬆️
router/pkg/metric/prom_connection_metric_store.go 75.75% <100.00%> (+2.42%) ⬆️
router/pkg/metric/connection_measurements.go 50.00% <0.00%> (-5.56%) ⬇️
router/internal/traceclient/traceclient.go 91.74% <84.56%> (-5.85%) ⬇️

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

🧹 Nitpick comments (4)
router/internal/traceclient/traceclient_test.go (3)

811-811: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use require.Same to assert the body is not wrapped.

require.Equal is a deep-value comparison; the invariant here is pointer identity, which Line 900 already expresses with require.Same.

♻️ Assert identity
-				require.Equal(t, tt.body, resp.Body)
+				require.Same(t, tt.body, resp.Body)
🤖 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/internal/traceclient/traceclient_test.go` at line 811, Update the body
assertion in the relevant traceclient test to use require.Same instead of
require.Equal, matching the identity assertion already used near line 900 and
verifying that resp.Body is the exact same object as tt.body.

596-600: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Absolute upper bound at Line 599 is timing-fragile.

Expected value is ~20ms against a 40ms ceiling; time.Sleep guarantees only a lower bound, so a loaded runner can push two 10ms paced reads past 40ms. The semantics you care about are already covered by the lower bound (Line 598) and the ordering assertion (Line 600). Consider widening the server-wait sleep and the ceiling, or dropping the absolute ceiling in favor of the relative comparison.

🤖 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/internal/traceclient/traceclient_test.go` around lines 596 - 600,
Remove the timing-fragile absolute upper-bound assertion on
results.TimeToLastByte in the test, relying on the existing lower-bound and
TimeToFirstByte ordering assertions to validate the intended semantics. Keep the
ttlb count and remaining timing checks unchanged.

107-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

pacedBody.Read silently drops chunk remainders on short buffers.

copy is bounded by len(p), but the chunk index advances unconditionally. Current call sites all read with buffers at least as large as each chunk, so no assertion is wrong today, but a future test reading in small increments would silently lose bytes and skew last-byte assertions.

♻️ Track the offset inside the current chunk
 type pacedBody struct {
 	chunks [][]byte
 	delay  time.Duration
 	i      int
+	off    int
 	closed bool
 }
 
 func (b *pacedBody) Read(p []byte) (int, error) {
 	if b.i >= len(b.chunks) {
 		return 0, io.EOF
 	}
 	time.Sleep(b.delay)
-	n := copy(p, b.chunks[b.i])
-	b.i++
+	n := copy(p, b.chunks[b.i][b.off:])
+	b.off += n
+	if b.off >= len(b.chunks[b.i]) {
+		b.i++
+		b.off = 0
+	}
 	return n, nil
 }
🤖 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/internal/traceclient/traceclient_test.go` around lines 107 - 115,
Update pacedBody.Read to retain an offset into the current chunk instead of
advancing b.i after every read. Copy only the remaining chunk bytes that fit in
p, advance the offset by n, and move to the next chunk only when the current
chunk is fully consumed, preserving io.EOF behavior after all chunks are
exhausted.
router/internal/traceclient/traceclient.go (1)

422-441: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Use errors.Is(err, io.EOF) for the completion check.

A body wrapper further down the chain can return an EOF wrapped in another error, in which case the == comparison misses completion and the metric is silently dropped.

♻️ Proposed change
 	complete := false
 	switch {
-	case err == io.EOF:
+	case errors.Is(err, io.EOF):
 		// A short EOF on a response with an explicit Content-Length is a
 		// truncated response, not a successfully observed last byte.
 		complete = b.contentLength < 0 || b.bytesRead >= b.contentLength

Add "errors" to the import block.

🤖 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/internal/traceclient/traceclient.go` around lines 422 - 441, Update
timedResponseBody.Read to detect wrapped EOF errors with errors.Is(err, io.EOF)
instead of direct equality, adding the errors import while preserving the
existing content-length completion logic.
🤖 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/internal/traceclient/traceclient_test.go`:
- Line 811: Update the body assertion in the relevant traceclient test to use
require.Same instead of require.Equal, matching the identity assertion already
used near line 900 and verifying that resp.Body is the exact same object as
tt.body.
- Around line 596-600: Remove the timing-fragile absolute upper-bound assertion
on results.TimeToLastByte in the test, relying on the existing lower-bound and
TimeToFirstByte ordering assertions to validate the intended semantics. Keep the
ttlb count and remaining timing checks unchanged.
- Around line 107-115: Update pacedBody.Read to retain an offset into the
current chunk instead of advancing b.i after every read. Copy only the remaining
chunk bytes that fit in p, advance the offset by n, and move to the next chunk
only when the current chunk is fully consumed, preserving io.EOF behavior after
all chunks are exhausted.

In `@router/internal/traceclient/traceclient.go`:
- Around line 422-441: Update timedResponseBody.Read to detect wrapped EOF
errors with errors.Is(err, io.EOF) instead of direct equality, adding the errors
import while preserving the existing content-length completion logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e55b0cf3-7e0f-4f74-85cc-0feb77e69e0a

📥 Commits

Reviewing files that changed from the base of the PR and between 6d7687e and 732b2ab.

📒 Files selected for processing (16)
  • docs-website/router/configuration.mdx
  • docs-website/router/configuration/template-expressions.mdx
  • docs-website/router/metrics-and-monitoring.mdx
  • docs-website/router/metrics-and-monitoring/prometheus-metric-reference.mdx
  • router-tests/observability/prometheus_test.go
  • router-tests/observability/structured_logging_test.go
  • router-tests/telemetry/connection_metrics_test.go
  • router/internal/expr/expr.go
  • router/internal/traceclient/traceclient.go
  • router/internal/traceclient/traceclient_test.go
  • router/pkg/config/config.schema.json
  • router/pkg/metric/connection_measurements.go
  • router/pkg/metric/connection_metric_store.go
  • router/pkg/metric/noop_connection_metrics.go
  • router/pkg/metric/oltp_connection_metric_store.go
  • router/pkg/metric/prom_connection_metric_store.go

@alepane21
alepane21 marked this pull request as ready for review August 6, 2026 10:18
@alepane21
alepane21 requested review from a team as code owners August 6, 2026 10:18
// This is always before the subgraph access log is written, because the
// engine fully reads and closes the body during the fetch load phase, which
// completes before the log is emitted.
if recorder != nil {

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.

Can you inverse this if statement so it becomes an early return?

}

func responseHasNoBody(req *http.Request, resp *http.Response) bool {
if req.Method == http.MethodHead || resp.Body == http.NoBody || resp.ContentLength == 0 {

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.

Confused on why req is part of this function. Isn't it only about resp ? If its a response to an HTTP HEAD, shouldn't the body still be empty so you don't need to check the req method specifically?

// lastByteMetricProvider is an optional extension implemented by the built-in
// OTLP and Prometheus providers. Keeping it separate avoids adding required
// methods to the exported ConnectionMetricProvider interface.
type lastByteMetricProvider interface {

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.

Any particular reason you chose a new interface here? You could also move these methods to ConnectionMetricProvider and get rid of this new interface

// LastByteMetricStore is an optional extension for first-to-last-byte transfer
// metrics. Implementations of ConnectionMetricStore that predate these metrics
// remain source-compatible.
type LastByteMetricStore interface {

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.

Any particular reason you chose a new interface here? You could also move these methods to ConnectionMetricStore and get rid of this new interface

@github-actions

Copy link
Copy Markdown

This PR was marked stale due to lack of activity. It will be closed in 14 days.

@github-actions github-actions Bot added Stale and removed Stale labels Aug 22, 2026
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.

2 participants