The default `urlstrictcolons=1` rejects URLs such as `http://localhost:1:2` or `http://::1/`.
Colons are permitted as part of a bracketed IPv6 address, such as `http://[::1]/`.
+Go 1.26 added a new `tracebacklabels` setting that controls the inclusion of
+goroutine labels set through the the `runtime/pprof` package. Setting `tracebacklabels=1`
+includes these key/value pairs in the goroutine status header of runtime
+tracebacks and debug=2 runtime/pprof stack dumps. This format may change in the future.
+(see go.dev/issue/76349)
+
### Go 1.25
Go 1.25 added a new `decoratemappings` setting that controls whether the Go
func printcomplex128(complex128)
func printcomplex64(complex64)
func printstring(string)
+func printquoted(string)
func printpointer(any)
func printuintptr(uintptr)
func printiface(any)
{"printcomplex128", funcTag, 27},
{"printcomplex64", funcTag, 29},
{"printstring", funcTag, 31},
+ {"printquoted", funcTag, 31},
{"printpointer", funcTag, 32},
{"printuintptr", funcTag, 33},
{"printiface", funcTag, 32},
if ir.IsConst(n, constant.String) {
cs = ir.StringVal(n)
}
- switch cs {
- case " ":
- on = typecheck.LookupRuntime("printsp")
- case "\n":
- on = typecheck.LookupRuntime("printnl")
- default:
- on = typecheck.LookupRuntime("printstring")
+ // Print values of the named type `quoted` using printquoted.
+ if types.RuntimeSymName(n.Type().Sym()) == "quoted" {
+ on = typecheck.LookupRuntime("printquoted")
+ } else {
+ switch cs {
+ case " ":
+ on = typecheck.LookupRuntime("printsp")
+ case "\n":
+ on = typecheck.LookupRuntime("printnl")
+ default:
+ on = typecheck.LookupRuntime("printstring")
+ }
}
default:
badtype(ir.OPRINT, n.Type(), nil)
{"runtime.printcomplex128", 1},
{"runtime.printcomplex64", 1},
{"runtime.printstring", 1},
+ {"runtime.printquoted", 1},
{"runtime.printpointer", 1},
{"runtime.printuintptr", 1},
{"runtime.printiface", 1},
internal/nettrace,
internal/platform,
internal/profilerecord,
+ internal/runtime/pprof/label,
internal/syslist,
internal/trace/tracev2,
internal/trace/traceviewer/format,
internal/goos,
internal/itoa,
internal/profilerecord,
+ internal/runtime/pprof/label,
internal/strconv,
internal/trace/tracev2,
math/bits,
< net/http/fcgi;
# Profiling
- FMT, compress/gzip, encoding/binary, sort, text/tabwriter
+ internal/runtime/pprof/label, runtime, context < internal/runtime/pprof;
+ FMT, compress/gzip, encoding/binary, sort, text/tabwriter, internal/runtime/pprof, internal/runtime/pprof/label
< runtime/pprof;
OS, compress/gzip, internal/lazyregexp
--- /dev/null
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package label provides common declarations used by both the [runtime] and [runtime/pprof] packages.
+// The [Set] type is used for goroutine labels, and is duplicated as
+// [runtime/pprof.LabelSet]. The type is duplicated due to go.dev/issue/65437
+// preventing the use of a type-alias in an existing public interface.
+package label
+
+// Label is a key/value pair of strings.
+type Label struct {
+ Key string
+ Value string
+}
+
+// Set is a set of labels.
+type Set struct {
+ List []Label
+}
+
+// NewSet constructs a LabelSet that wraps the provided labels.
+func NewSet(list []Label) Set {
+ return Set{List: list}
+}
}
return string(buf[:n])
}
+
+// DumpPrintQuoted provides access to print(quoted()) for the tests in
+// runtime/print_quoted_test.go, allowing us to test that implementation.
+func DumpPrintQuoted(s string) string {
+ gp := getg()
+ gp.writebuf = make([]byte, 0, 1<<20)
+ print(quoted(s))
+ buf := gp.writebuf
+ gp.writebuf = nil
+
+ return string(buf)
+}
import (
"context"
"fmt"
+ "internal/runtime/pprof/label"
"slices"
"strings"
)
-type label struct {
- key string
- value string
-}
-
// LabelSet is a set of labels.
type LabelSet struct {
- list []label
+ list []label.Label
}
// labelContextKey is the type of contextKeys used for profiler labels.
// This is an initial implementation, but it will be replaced with something
// that admits incremental immutable modification more efficiently.
type labelMap struct {
- LabelSet
+ label.Set
}
// String satisfies Stringer and returns key, value pairs in a consistent
if l == nil {
return ""
}
- keyVals := make([]string, 0, len(l.list))
+ keyVals := make([]string, 0, len(l.Set.List))
- for _, lbl := range l.list {
- keyVals = append(keyVals, fmt.Sprintf("%q:%q", lbl.key, lbl.value))
+ for _, lbl := range l.Set.List {
+ keyVals = append(keyVals, fmt.Sprintf("%q:%q", lbl.Key, lbl.Value))
}
slices.Sort(keyVals)
// A label overwrites a prior label with the same key.
func WithLabels(ctx context.Context, labels LabelSet) context.Context {
parentLabels := labelValue(ctx)
- return context.WithValue(ctx, labelContextKey{}, &labelMap{mergeLabelSets(parentLabels.LabelSet, labels)})
+ return context.WithValue(ctx, labelContextKey{}, &labelMap{mergeLabelSets(parentLabels.Set, labels)})
}
-func mergeLabelSets(left, right LabelSet) LabelSet {
- if len(left.list) == 0 {
- return right
+func mergeLabelSets(left label.Set, right LabelSet) label.Set {
+ if len(left.List) == 0 {
+ return label.NewSet(right.list)
} else if len(right.list) == 0 {
return left
}
+ lList, rList := left.List, right.list
l, r := 0, 0
- result := make([]label, 0, len(right.list))
- for l < len(left.list) && r < len(right.list) {
- switch strings.Compare(left.list[l].key, right.list[r].key) {
+ result := make([]label.Label, 0, len(rList))
+ for l < len(lList) && r < len(rList) {
+ switch strings.Compare(lList[l].Key, rList[r].Key) {
case -1: // left key < right key
- result = append(result, left.list[l])
+ result = append(result, lList[l])
l++
case 1: // right key < left key
- result = append(result, right.list[r])
+ result = append(result, rList[r])
r++
case 0: // keys are equal, right value overwrites left value
- result = append(result, right.list[r])
+ result = append(result, rList[r])
l++
r++
}
}
// Append the remaining elements
- result = append(result, left.list[l:]...)
- result = append(result, right.list[r:]...)
+ result = append(result, lList[l:]...)
+ result = append(result, rList[r:]...)
- return LabelSet{list: result}
+ return label.NewSet(result)
}
// Labels takes an even number of strings representing key-value pairs
if len(args)%2 != 0 {
panic("uneven number of arguments to pprof.Labels")
}
- list := make([]label, 0, len(args)/2)
+ list := make([]label.Label, 0, len(args)/2)
sortedNoDupes := true
for i := 0; i+1 < len(args); i += 2 {
- list = append(list, label{key: args[i], value: args[i+1]})
+ list = append(list, label.Label{Key: args[i], Value: args[i+1]})
sortedNoDupes = sortedNoDupes && (i < 2 || args[i] > args[i-2])
}
if !sortedNoDupes {
// slow path: keys are unsorted, contain duplicates, or both
- slices.SortStableFunc(list, func(a, b label) int {
- return strings.Compare(a.key, b.key)
+ slices.SortStableFunc(list, func(a, b label.Label) int {
+ return strings.Compare(a.Key, b.Key)
})
- deduped := make([]label, 0, len(list))
+ deduped := make([]label.Label, 0, len(list))
for i, lbl := range list {
- if i == 0 || lbl.key != list[i-1].key {
+ if i == 0 || lbl.Key != list[i-1].Key {
deduped = append(deduped, lbl)
} else {
deduped[len(deduped)-1] = lbl
// whether that label exists.
func Label(ctx context.Context, key string) (string, bool) {
ctxLabels := labelValue(ctx)
- for _, lbl := range ctxLabels.list {
- if lbl.key == key {
- return lbl.value, true
+ for _, lbl := range ctxLabels.Set.List {
+ if lbl.Key == key {
+ return lbl.Value, true
}
}
return "", false
// The function f should return true to continue iteration or false to stop iteration early.
func ForLabels(ctx context.Context, f func(key, value string) bool) {
ctxLabels := labelValue(ctx)
- for _, lbl := range ctxLabels.list {
- if !f(lbl.key, lbl.value) {
+ for _, lbl := range ctxLabels.Set.List {
+ if !f(lbl.Key, lbl.Value) {
break
}
}
import (
"context"
"fmt"
+ "internal/runtime/pprof/label"
"reflect"
"slices"
"strings"
"testing"
)
-func labelsSorted(ctx context.Context) []label {
- ls := []label{}
+func labelsSorted(ctx context.Context) []label.Label {
+ ls := []label.Label{}
ForLabels(ctx, func(key, value string) bool {
- ls = append(ls, label{key, value})
+ ls = append(ls, label.Label{Key: key, Value: value})
return true
})
- slices.SortFunc(ls, func(a, b label) int { return strings.Compare(a.key, b.key) })
+ slices.SortFunc(ls, func(a, b label.Label) int { return strings.Compare(a.Key, b.Key) })
return ls
}
t.Errorf(`Label(ctx, "key"): got %v, %v; want "value", ok`, v, ok)
}
gotLabels := labelsSorted(ctx)
- wantLabels := []label{{"key", "value"}}
+ wantLabels := []label.Label{{Key: "key", Value: "value"}}
if !reflect.DeepEqual(gotLabels, wantLabels) {
t.Errorf("(sorted) labels on context: got %v, want %v", gotLabels, wantLabels)
}
t.Errorf(`Label(ctx, "key2"): got %v, %v; want "value2", ok`, v, ok)
}
gotLabels = labelsSorted(ctx)
- wantLabels = []label{{"key", "value"}, {"key2", "value2"}}
+ wantLabels = []label.Label{{Key: "key", Value: "value"}, {Key: "key2", Value: "value2"}}
if !reflect.DeepEqual(gotLabels, wantLabels) {
t.Errorf("(sorted) labels on context: got %v, want %v", gotLabels, wantLabels)
}
t.Errorf(`Label(ctx, "key3"): got %v, %v; want "value3", ok`, v, ok)
}
gotLabels = labelsSorted(ctx)
- wantLabels = []label{{"key", "value3"}, {"key2", "value2"}}
+ wantLabels = []label.Label{{Key: "key", Value: "value3"}, {Key: "key2", Value: "value2"}}
if !reflect.DeepEqual(gotLabels, wantLabels) {
t.Errorf("(sorted) labels on context: got %v, want %v", gotLabels, wantLabels)
}
t.Errorf(`Label(ctx, "key4"): got %v, %v; want "value4b", ok`, v, ok)
}
gotLabels = labelsSorted(ctx)
- wantLabels = []label{{"key", "value3"}, {"key2", "value2"}, {"key4", "value4b"}}
+ wantLabels = []label.Label{{Key: "key", Value: "value3"}, {Key: "key2", Value: "value2"}, {Key: "key4", Value: "value4b"}}
if !reflect.DeepEqual(gotLabels, wantLabels) {
t.Errorf("(sorted) labels on context: got %v, want %v", gotLabels, wantLabels)
}
expected: "{}",
}, {
m: labelMap{
- Labels("foo", "bar"),
+ label.NewSet(Labels("foo", "bar").list),
},
expected: `{"foo":"bar"}`,
}, {
m: labelMap{
- Labels(
+ label.NewSet(Labels(
"foo", "bar",
"key1", "value1",
"key2", "value2",
"key3", "value3",
"key4WithNewline", "\nvalue4",
- ),
+ ).list),
},
expected: `{"foo":"bar", "key1":"value1", "key2":"value2", "key3":"value3", "key4WithNewline":"\nvalue4"}`,
},
var labels func()
if p.Label(idx) != nil {
labels = func() {
- for _, lbl := range p.Label(idx).list {
- b.pbLabel(tagSample_Label, lbl.key, lbl.value, 0)
+ for _, lbl := range p.Label(idx).Set.List {
+ b.pbLabel(tagSample_Label, lbl.Key, lbl.Value, 0)
}
}
}
"fmt"
"internal/abi"
"internal/profile"
+ "internal/runtime/pprof/label"
"internal/syscall/unix"
"internal/testenv"
"io"
goroutineProf.WriteTo(&w, 1)
prof := w.String()
- labels := labelMap{Labels("label", "value")}
+ labels := labelMap{label.NewSet(Labels("label", "value").list)}
labelStr := "\n# labels: " + labels.String()
- selfLabel := labelMap{Labels("self-label", "self-value")}
+ selfLabel := labelMap{label.NewSet(Labels("self-label", "self-value").list)}
selfLabelStr := "\n# labels: " + selfLabel.String()
- fingLabel := labelMap{Labels("fing-label", "fing-value")}
+ fingLabel := labelMap{label.NewSet(Labels("fing-label", "fing-value").list)}
fingLabelStr := "\n# labels: " + fingLabel.String()
orderedPrefix := []string{
"\n50 @ ",
var labels func()
if e.tag != nil {
labels = func() {
- for _, lbl := range (*labelMap)(e.tag).list {
- b.pbLabel(tagSample_Label, lbl.key, lbl.value, 0)
+ for _, lbl := range (*labelMap)(e.tag).Set.List {
+ b.pbLabel(tagSample_Label, lbl.Key, lbl.Value, 0)
}
}
}
if l == nil {
return map[string]string{}
}
- m := make(map[string]string, len(l.list))
- for _, lbl := range l.list {
- m[lbl.key] = lbl.value
+ ls := l.Set.List
+ m := make(map[string]string, len(ls))
+ for _, lbl := range ls {
+ m[lbl.Key] = lbl.Value
}
return m
}
// should use printhex instead of printuint (decimal).
type hex uint64
+// The compiler knows that a print of a value of this type should use
+// printquoted instead of printstring.
+type quoted string
+
func bytes(s string) (ret []byte) {
rp := (*slice)(unsafe.Pointer(&ret))
sp := stringStructOf(&s)
var minhexdigits = 0 // protected by printlock
-func printhex(v uint64) {
+func printhexopts(include0x bool, mindigits int, v uint64) {
const dig = "0123456789abcdef"
var buf [100]byte
i := len(buf)
for i--; i > 0; i-- {
buf[i] = dig[v%16]
- if v < 16 && len(buf)-i >= minhexdigits {
+ if v < 16 && len(buf)-i >= mindigits {
break
}
v /= 16
}
- i--
- buf[i] = 'x'
- i--
- buf[i] = '0'
+ if include0x {
+ i--
+ buf[i] = 'x'
+ i--
+ buf[i] = '0'
+ }
gwrite(buf[i:])
}
+func printhex(v uint64) {
+ printhexopts(true, minhexdigits, v)
+}
+
+func printquoted(s string) {
+ printlock()
+ gwrite([]byte(`"`))
+ for _, r := range s {
+ switch r {
+ case '\n':
+ gwrite([]byte(`\n`))
+ continue
+ case '\r':
+ gwrite([]byte(`\r`))
+ continue
+ case '\t':
+ gwrite([]byte(`\t`))
+ print()
+ continue
+ case '\\', '"':
+ gwrite([]byte{byte('\\'), byte(r)})
+ continue
+ }
+ // For now, only allow basic printable ascii through unescaped
+ if r >= ' ' && r <= '~' {
+ gwrite([]byte{byte(r)})
+ } else if r < 127 {
+ gwrite(bytes(`\x`))
+ printhexopts(false, 2, uint64(r))
+ } else if r < 0x1_0000 {
+ gwrite(bytes(`\u`))
+ printhexopts(false, 4, uint64(r))
+ } else {
+ gwrite(bytes(`\U`))
+ printhexopts(false, 8, uint64(r))
+ }
+ }
+ gwrite([]byte{byte('"')})
+ printunlock()
+}
+
func printpointer(p unsafe.Pointer) {
printhex(uint64(uintptr(p)))
}
--- /dev/null
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package runtime_test
+
+import (
+ "runtime"
+ "testing"
+)
+
+func TestPrintQuoted(t *testing.T) {
+ for _, tbl := range []struct {
+ in, expected string
+ }{
+ {in: "baz", expected: `"baz"`},
+ {in: "foobar", expected: `"foobar"`},
+ // make sure newlines get escaped
+ {in: "baz\n", expected: `"baz\n"`},
+ // make sure null and escape bytes are properly escaped
+ {in: "b\033it", expected: `"b\x1bit"`},
+ {in: "b\000ar", expected: `"b\x00ar"`},
+ // verify that simple 16-bit unicode runes are escaped with \u, including a greek upper-case sigma and an arbitrary unicode character.
+ {in: "\u1234Σ", expected: `"\u1234\u03a3"`},
+ // verify that 32-bit unicode runes are escaped with \U along with tabs
+ {in: "fizz\tle", expected: `"fizz\tle"`},
+ {in: "\U00045678boop", expected: `"\U00045678boop"`},
+ // verify carriage returns and backslashes get escaped along with our nulls, newlines and a 32-bit unicode character
+ {in: "fiz\\zl\re", expected: `"fiz\\zl\re"`},
+ } {
+ t.Run(tbl.in, func(t *testing.T) {
+ out := runtime.DumpPrintQuoted(tbl.in)
+ if out != tbl.expected {
+ t.Errorf("unexpected output for print(escaped(%q));\n got: %s\nwant: %s", tbl.in, out, tbl.expected)
+ }
+ })
+ }
+}
// but allowing it is convenient for testing and for programs
// that do an os.Setenv in main.init or main.main.
asynctimerchan atomic.Int32
+
+ // tracebacklabels controls the inclusion of goroutine labels in the
+ // goroutine status header line.
+ tracebacklabels atomic.Int32
}
var dbgvars = []*dbgVar{
{name: "traceallocfree", atomic: &debug.traceallocfree},
{name: "tracecheckstackownership", value: &debug.traceCheckStackOwnership},
{name: "tracebackancestors", value: &debug.tracebackancestors},
+ {name: "tracebacklabels", atomic: &debug.tracebacklabels, def: 0},
{name: "tracefpunwindoff", value: &debug.tracefpunwindoff},
{name: "updatemaxprocs", value: &debug.updatemaxprocs, def: 1},
}
"internal/abi"
"internal/bytealg"
"internal/goarch"
+ "internal/runtime/pprof/label"
"internal/runtime/sys"
"internal/stringslite"
"unsafe"
if bubble := gp.bubble; bubble != nil {
print(", synctest bubble ", bubble.id)
}
+ if gp.labels != nil && debug.tracebacklabels.Load() == 1 {
+ labels := (*label.Set)(gp.labels).List
+ if len(labels) > 0 {
+ print(" labels:{")
+ for i, kv := range labels {
+ print(quoted(kv.Key), ": ", quoted(kv.Value))
+ if i < len(labels)-1 {
+ print(", ")
+ }
+ }
+ print("}")
+ }
+ }
print("]:\n")
}
import (
"bytes"
+ "context"
"fmt"
"internal/abi"
"internal/asan"
"regexp"
"runtime"
"runtime/debug"
+ "runtime/pprof"
"strconv"
"strings"
"sync"
t.Fatalf("want %s, got %s\n", want, output)
}
}
+
+func TestTracebackGoroutineLabels(t *testing.T) {
+ t.Setenv("GODEBUG", "tracebacklabels=1")
+ for _, tbl := range []struct {
+ l pprof.LabelSet
+ expTB string
+ }{
+ {l: pprof.Labels("foobar", "baz"), expTB: `{"foobar": "baz"}`},
+ // Make sure the keys are sorted because the runtime/pprof package sorts for consistency
+ {l: pprof.Labels("foobar", "baz", "fizzle", "bit"), expTB: `{"fizzle": "bit", "foobar": "baz"}`},
+ // make sure newlines get escaped
+ {l: pprof.Labels("fizzle", "bit", "foobar", "baz\n"), expTB: `{"fizzle": "bit", "foobar": "baz\n"}`},
+ // make sure null and escape bytes are properly escaped
+ {l: pprof.Labels("fizzle", "b\033it", "foo\"ba\x00r", "baz\n"), expTB: `{"fizzle": "b\x1bit", "foo\"ba\x00r": "baz\n"}`},
+ // verify that simple 16-bit unicode runes are escaped with \u, including a greek upper-case sigma and an arbitrary unicode character.
+ {l: pprof.Labels("fizzle", "\u1234Σ", "fooba\x00r", "baz\n"), expTB: `{"fizzle": "\u1234\u03a3", "fooba\x00r": "baz\n"}`},
+ // verify that 32-bit unicode runes are escaped with \U along with tabs
+ {l: pprof.Labels("fizz\tle", "\U00045678boop", "fooba\x00r", "baz\n"), expTB: `{"fizz\tle": "\U00045678boop", "fooba\x00r": "baz\n"}`},
+ // verify carriage returns and backslashes get escaped along with our nulls, newlines and a 32-bit unicode character
+ {l: pprof.Labels("fiz\\zl\re", "\U00045678boop", "fooba\x00r", "baz\n"), expTB: `{"fiz\\zl\re": "\U00045678boop", "fooba\x00r": "baz\n"}`},
+ } {
+ t.Run(tbl.expTB, func(t *testing.T) {
+ verifyLabels := func() {
+ t.Helper()
+ buf := make([]byte, 1<<10)
+ // We collect the stack only for this goroutine (by passing
+ // false to runtime.Stack). We expect to see the parent's goroutine labels in the traceback.
+ stack := string(buf[:runtime.Stack(buf, false)])
+ if !strings.Contains(stack, "labels:"+tbl.expTB) {
+ t.Errorf("failed to find goroutine labels with labels %s (as %s) got:\n%s\n---", tbl.l, tbl.expTB, stack)
+ }
+ }
+ // Use a clean context so the testing package can add whatever goroutine labels it wants to the testing.T context.
+ lblCtx := pprof.WithLabels(context.Background(), tbl.l)
+ pprof.SetGoroutineLabels(lblCtx)
+ var wg sync.WaitGroup
+ // make sure the labels are visible in a child goroutine
+ wg.Go(verifyLabels)
+ // and in this parent goroutine
+ verifyLabels()
+ wg.Wait()
+ })
+ }
+}
+
+func TestTracebackGoroutineLabelsDisabledGODEBUG(t *testing.T) {
+ t.Setenv("GODEBUG", "tracebacklabels=0")
+ lbls := pprof.Labels("foobar", "baz")
+ verifyLabels := func() {
+ t.Helper()
+ buf := make([]byte, 1<<10)
+ // We collect the stack only for this goroutine (by passing
+ // false to runtime.Stack).
+ stack := string(buf[:runtime.Stack(buf, false)])
+ if strings.Contains(stack, "labels:") {
+ t.Errorf("found goroutine labels with labels %s got:\n%s\n---", lbls, stack)
+ }
+ }
+ // Use a clean context so the testing package can add whatever goroutine labels it wants to the testing.T context.
+ lblCtx := pprof.WithLabels(context.Background(), lbls)
+ pprof.SetGoroutineLabels(lblCtx)
+ var wg sync.WaitGroup
+ // make sure the labels are visible in a child goroutine
+ wg.Go(verifyLabels)
+ // and in this parent goroutine
+ verifyLabels()
+ wg.Wait()
+}