control/controlclient: add patchify miss stats

Add an opt-in metrics.LabelMap tracking why patchifyPeer fails to
convert a PeersChanged entry into a PeersChangedPatch. The stats are
gated behind the TS_DEBUG_PATCHIFY_PEER_MISS envknob so there is zero
overhead in normal operation.

peerChangeDiff now takes an optional onFalse callback that is called
with the field name on every non-patchable return path. When the
envknob is off, nil is passed and replaced with a no-op at the top of
peerChangeDiff.

The resulting metric renders as:

    counter_patchify_miss{why="Hostinfo"} 2
    counter_patchify_miss{why="peer_not_found"} 1170

Updates tailscale/corp#40088

Change-Id: I2d4b9074bf42ec03ab296c0629a54106bafa873e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-04-15 14:37:33 +00:00
committed by Brad Fitzpatrick
parent 61c95f409c
commit dbf468740b
3 changed files with 59 additions and 6 deletions
+2 -2
View File
@@ -1231,7 +1231,7 @@ func TestPeerChangeDiff(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pc, ok := peerChangeDiff(tt.a.View(), tt.b)
pc, ok := peerChangeDiff(tt.a.View(), tt.b, nil)
if tt.wantEqual {
if !ok || pc != nil {
t.Errorf("got (%p, %v); want (nil, true); pc=%v", pc, ok, logger.AsJSON(pc))
@@ -1252,7 +1252,7 @@ func TestPeerChangeDiffAllocs(t *testing.T) {
a := &tailcfg.Node{ID: 1}
b := &tailcfg.Node{ID: 1}
n := testing.AllocsPerRun(10000, func() {
diff, ok := peerChangeDiff(a.View(), b)
diff, ok := peerChangeDiff(a.View(), b, nil)
if !ok || diff != nil {
t.Fatalf("unexpected result: (%s, %v)", logger.AsJSON(diff), ok)
}