Let a host name the span a call continues

A host that keeps spans in its own tracer opens positions zip never
sees: a span inside a handler, or work that outlives its request. An
outbound call then names the request, or nothing, as its parent, and
the callee's spans land beside the span that caused them.

Claude-Session: https://claude.ai/code/session_01HuMDfgFssPUptYUzgZ6Cp8
This commit is contained in:
2026-09-24 14:18:13 -07:00
parent fc957a2bdb
commit 55db4f138b
3 changed files with 72 additions and 0 deletions
+5
View File
@@ -202,6 +202,11 @@ func forwardIdentity(ctx context.Context, req *fasthttp.Request) {
req.Header.SetBytesV(HeaderTrace, v)
}
}
if Traceparent != nil {
if v := Traceparent(ctx); v != "" {
req.Header.Set(HeaderTrace, v)
}
}
if acting, ok := ctx.Value(actingKey{}).(Caller); ok {
for h, v := range acting.headers() {
req.Header.Set(h, v)
+11
View File
@@ -63,6 +63,17 @@ import (
// that is.
const HeaderTrace = "traceparent"
// Traceparent, when set, answers the trace position a call made with ctx
// continues, as a W3C traceparent value, or "" for none.
//
// A host that keeps spans in a tracer of its own has positions zip never sees: a
// span opened inside a handler, or work that outlives the request it began in and
// carries no request at all. Without this an outbound call names the inbound
// request as its parent, or nothing, and the callee's spans land beside the span
// that caused them instead of under it. A non-empty answer wins over the
// request's own header. Set it once, before the first call.
var Traceparent func(ctx context.Context) string
// traceVersion is the only trace-context version this parses. The spec says a
// receiver must accept future versions by reading the fields it knows, but a
// version it does not know may reorder them — so an unknown version is treated
+56
View File
@@ -0,0 +1,56 @@
package zip
import (
"context"
"net/http/httptest"
"testing"
"github.com/valyala/fasthttp"
)
// sentTrace is the traceparent forwardIdentity writes onto an outbound request.
func sentTrace(ctx context.Context) string {
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
forwardIdentity(ctx, req)
return string(req.Header.Peek(HeaderTrace))
}
// A host's own trace position rides a call that has no request behind it, wins
// over the request's when there is one, and an empty answer leaves the request's.
func TestTraceparentHook(t *testing.T) {
const own = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
old := Traceparent
t.Cleanup(func() { Traceparent = old })
Traceparent = nil
if got := sentTrace(context.Background()); got != "" {
t.Fatalf("no hook, no request: sent %q, want nothing", got)
}
Traceparent = func(context.Context) string { return own }
if got := sentTrace(context.Background()); got != own {
t.Fatalf("detached call: sent %q, want %q", got, own)
}
var fromRequest, overridden string
app := New(Config{AppName: "front", DisableStartupMessage: true})
app.Raw("GET", "/ask", func(c *Ctx) error {
Traceparent = func(context.Context) string { return "" }
fromRequest = sentTrace(c.Forward())
Traceparent = func(context.Context) string { return own }
overridden = sentTrace(c.Forward())
return c.JSON(200, map[string]string{"ok": "1"})
})
resp, err := app.Fiber().Test(httptest.NewRequest("GET", "/ask", nil))
if err != nil {
t.Fatalf("GET: %v", err)
}
_ = resp.Body.Close()
if fromRequest == "" || fromRequest == own {
t.Fatalf("empty answer: sent %q, want the request's own traceparent", fromRequest)
}
if overridden != own {
t.Fatalf("hook answer: sent %q, want %q over the request's", overridden, own)
}
}