Skip to content

feat(tracing): HTTP response status on spans, sanitize error recording, bump semconv to v1.27.0#4879

Merged
lpcox merged 4 commits intomainfrom
copilot/go-fan-module-review
Apr 30, 2026
Merged

feat(tracing): HTTP response status on spans, sanitize error recording, bump semconv to v1.27.0#4879
lpcox merged 4 commits intomainfrom
copilot/go-fan-module-review

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 30, 2026

Three OTel best-practice gaps identified in the Go Fan module review: WrapHTTPHandler never recorded the HTTP response status code, SetStatus and RecordError were leaking raw error strings to trace backends, and all files were pinned to semconv v1.26.0 despite v1.27.0 being available.

Changes

  • tracing/http.go — capture HTTP response status on spans

    • Added statusResponseWriter wrapper intercepting WriteHeader and Write (handles implicit 200 when only Write is called)
    • Added Unwrap() http.ResponseWriter to preserve optional interfaces (http.Flusher, http.Hijacker, etc.) so streaming/SSE handlers are not broken
    • Sets http.response.status_code attribute and codes.Error span status for 5xx via defer, ensuring attributes are recorded even if the handler panics
  • server/unified.go — sanitize error recording

    • execSpan.SetStatus(codes.Error, err.Error())execSpan.SetStatus(codes.Error, "tool execution failed")
    • execSpan.RecordError(err)execSpan.RecordError(fmt.Errorf("tool execution failed")) — prevents raw upstream error text from appearing as exception.message in trace backends; the full error is still returned to the caller via mcp.NewErrorCallToolResult(err)
  • semconv v1.26.0v1.27.0 across all four affected files (tracing/http.go, tracing/provider.go, server/unified.go, proxy/handler.go); no go.mod/go.sum changes required as semconv is a sub-package of the already-imported go.opentelemetry.io/otel v1.43.0

  • Tests — added three new test cases to internal/tracing/provider_test.go covering the HTTP status recording behaviour:

    • Explicit WriteHeader(404) is reflected in the span attribute; 4xx does not set codes.Error
    • Implicit 200 via Write without WriteHeader is correctly recorded
    • 5xx sets span status to codes.Error with a non-empty description

Copilot AI linked an issue Apr 30, 2026 that may be closed by this pull request
4 tasks
Copilot AI and others added 2 commits April 30, 2026 14:04
…ump semconv to v1.27.0

Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/111ffb27-9d69-41be-b824-8c1882bf7279

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copilot AI changed the title [WIP] Review OpenTelemetry Go module go.opentelemetry.io/otel feat(tracing): HTTP response status on spans, sanitize SetStatus description, bump semconv to v1.27.0 Apr 30, 2026
Copilot AI requested a review from lpcox April 30, 2026 14:07
@lpcox lpcox marked this pull request as ready for review April 30, 2026 14:47
Copilot AI review requested due to automatic review settings April 30, 2026 14:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR tightens OpenTelemetry tracing behavior in the gateway/proxy by recording HTTP response status codes on server spans, reducing sensitive error text exposure via span status descriptions, and updating the semconv import version used across the codebase.

Changes:

  • Add an http.ResponseWriter wrapper in WrapHTTPHandler to capture and attach http.response.status_code, and set codes.Error for 5xx responses.
  • Replace SetStatus(..., err.Error()) with a generic status description for backend tool execution spans.
  • Bump semconv imports from v1.26.0 to v1.27.0 in the affected files.
Show a summary per file
File Description
internal/tracing/http.go Wraps ResponseWriter to capture/set HTTP response status code on spans and mark 5xx as errors.
internal/tracing/provider.go Updates semconv import used for resource schema URL and service attributes.
internal/server/unified.go Sanitizes span status description for backend tool execution errors; updates semconv import.
internal/proxy/handler.go Updates semconv import used for span attributes in proxy request tracing.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comments suppressed due to low confidence (2)

internal/tracing/http.go:25

  • WriteHeader currently overwrites rw.statusCode on every call, but net/http only sends the first status code (subsequent WriteHeader calls are ignored). This can cause the span to record a status code that was never actually written to the client. Track whether headers have already been written (or only set statusCode on the first call) so the recorded status matches the real response.
func (rw *statusResponseWriter) WriteHeader(code int) {
	rw.statusCode = code
	rw.ResponseWriter.WriteHeader(code)
}

internal/tracing/http.go:33

  • statusResponseWriter is initialized with statusCode=http.StatusOK, so the rw.statusCode==0 check in Write is dead code and the comment about “implicit 200 when Write is called without WriteHeader” is misleading. Either initialize statusCode to 0 and set it on the first Write/WriteHeader (and treat 0 as “not written yet”), or remove the conditional/comment to keep the wrapper’s behavior and documentation consistent.
// Write captures an implicit 200 status when Write is called without a prior WriteHeader.
func (rw *statusResponseWriter) Write(b []byte) (int, error) {
	if rw.statusCode == 0 {
		rw.statusCode = http.StatusOK
	}
	return rw.ResponseWriter.Write(b)
}
  • Files reviewed: 4/4 changed files
  • Comments generated: 4

Comment thread internal/tracing/http.go
Comment on lines +16 to +20
// statusResponseWriter wraps http.ResponseWriter to capture the HTTP response status code.
type statusResponseWriter struct {
http.ResponseWriter
statusCode int
}
Comment thread internal/tracing/http.go
Comment on lines +71 to +76
srw := &statusResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
defer func() {
span.SetAttributes(semconv.HTTPResponseStatusCodeKey.Int(srw.statusCode))
if srw.statusCode >= 500 {
span.SetStatus(codes.Error, http.StatusText(srw.statusCode))
}
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
semconv "go.opentelemetry.io/otel/semconv/v1.27.0"
Comment thread internal/server/unified.go Outdated
Comment on lines 555 to 558
execSpan.RecordError(err)
execSpan.SetStatus(codes.Error, err.Error())
execSpan.SetStatus(codes.Error, "tool execution failed")
httpStatusCode = 500
return mcp.NewErrorCallToolResult(err)
@lpcox
Copy link
Copy Markdown
Collaborator

lpcox commented Apr 30, 2026

@copilot address the review feedback

…r, add status tests

Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/846b06ec-3baf-4a96-879e-dc60abf3081d

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 30, 2026

@copilot address the review feedback

Addressed all four review points in commit 9e66b78:

  1. Unwrap() on statusResponseWriter — added Unwrap() http.ResponseWriter so http.ResponseController and interface assertions (Flusher, Hijacker, etc.) still reach the underlying writer through the wrapper.

  2. Tests for HTTP status recording — added three new test functions to provider_test.go:

    • TestWrapHTTPHandler_RecordsExplicitStatusCode — explicit WriteHeader(404) is reflected on the span, and 4xx does not set codes.Error
    • TestWrapHTTPHandler_RecordsImplicit200Write without WriteHeader records status 200
    • TestWrapHTTPHandler_5xxSetsSpanStatusError — 5xx sets span status to codes.Error with a non-empty description
  3. go mod tidy — ran cleanly with no go.mod/go.sum changes; semconv/v1.27.0 is a sub-package within the already-imported go.opentelemetry.io/otel v1.43.0 module.

  4. Sanitize RecordError — replaced execSpan.RecordError(err) with execSpan.RecordError(fmt.Errorf("tool execution failed")) so the trace backend receives a generic exception.message consistent with the SetStatus description. The real error is still returned to the caller via mcp.NewErrorCallToolResult(err) and captured in server-side logs.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /tmp/go-build3552668569/b513/launcher.test /tmp/go-build3552668569/b513/launcher.test -test.testlogfile=/tmp/go-build3552668569/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build3552668569/b248/vet.cfg g_.a 6880639/b268/ x_amd64/compile --gdwarf-5 channelz -o x_amd64/compile 6880�� QxcRrRcx- .cfg x_amd64/vet 6880639/b183/ -O2 (dns block)
  • invalid-host-that-does-not-exist-12345.com
    • Triggering command: /tmp/go-build3552668569/b495/config.test /tmp/go-build3552668569/b495/config.test -test.testlogfile=/tmp/go-build3552668569/b495/testlog.txt -test.paniconexit0 -test.timeout=10m0s -I /opt/hostedtoolcache/go/1.25.9/x64/src/runtime/c-c=4 es.go x_amd64/compile --gdwarf-5 --64 6880639/b268/ x_amd64/compile ctor�� ache/go/1.25.9/x64/src/os/user .cfg x_amd64/vet --gdwarf-5 ut-828214921.c -o x_amd64/vet (dns block)
  • nonexistent.local
    • Triggering command: /tmp/go-build3552668569/b513/launcher.test /tmp/go-build3552668569/b513/launcher.test -test.testlogfile=/tmp/go-build3552668569/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build3552668569/b248/vet.cfg g_.a 6880639/b268/ x_amd64/compile --gdwarf-5 channelz -o x_amd64/compile 6880�� QxcRrRcx- .cfg x_amd64/vet 6880639/b183/ -O2 (dns block)
  • slow.example.com
    • Triggering command: /tmp/go-build3552668569/b513/launcher.test /tmp/go-build3552668569/b513/launcher.test -test.testlogfile=/tmp/go-build3552668569/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build3552668569/b248/vet.cfg g_.a 6880639/b268/ x_amd64/compile --gdwarf-5 channelz -o x_amd64/compile 6880�� QxcRrRcx- .cfg x_amd64/vet 6880639/b183/ -O2 (dns block)
  • this-host-does-not-exist-12345.com
    • Triggering command: /tmp/go-build3552668569/b522/mcp.test /tmp/go-build3552668569/b522/mcp.test -test.testlogfile=/tmp/go-build3552668569/b522/testlog.txt -test.paniconexit0 -test.timeout=10m0s -W olang.org/grpc@v1.80.0/tap/tap.g-c=4 .cfg x_amd64/vet s-through=-lgcc s-through=-lgcc_-atomic s-through=-lpthr-bool x_amd64/vet s-th�� 6880639/b397/_pk-errorsas /tmp/go-build251-ifaceassert x_amd64/vet . g/grpc/internal/-qE (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title feat(tracing): HTTP response status on spans, sanitize SetStatus description, bump semconv to v1.27.0 feat(tracing): HTTP response status on spans, sanitize error recording, bump semconv to v1.27.0 Apr 30, 2026
@lpcox lpcox merged commit 5216c6c into main Apr 30, 2026
18 checks passed
@lpcox lpcox deleted the copilot/go-fan-module-review branch April 30, 2026 15:14
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.

[go-fan] Go Module Review: go.opentelemetry.io/otel

3 participants