Skip to content

Commit 8ed4fd1

Browse files
committed
envknob/logknob: add package for configurable logging
A LogKnob allows enabling logs with an envknob, netmap capability, and manually, and calling a logging function when logs are enabled. Signed-off-by: Andrew Dunham <[email protected]> Change-Id: Id66c608d4e488bfd4eaa5e867a8d9289686748be
1 parent 3b39ca9 commit 8ed4fd1

File tree

3 files changed

+197
-0
lines changed

3 files changed

+197
-0
lines changed

envknob/logknob/logknob.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
// Package logknob provides a helpful wrapper that allows enabling logging
5+
// based on either an envknob or other methods of enablement.
6+
package logknob
7+
8+
import (
9+
"sync/atomic"
10+
11+
"golang.org/x/exp/slices"
12+
"tailscale.com/envknob"
13+
"tailscale.com/types/logger"
14+
)
15+
16+
// TODO(andrew-d): should we have a package-global registry of logknobs? It
17+
// would allow us to update from a netmap in a central location, which might be
18+
// reason enough to do it...
19+
20+
// LogKnob allows configuring verbose logging, with multiple ways to enable. It
21+
// supports enabling logging via envknob, via atomic boolean (for use in e.g.
22+
// c2n log level changes), and via capabilities from a NetMap (so users can
23+
// enable logging via the ACL JSON).
24+
type LogKnob struct {
25+
capName string
26+
cap atomic.Bool
27+
env func() bool
28+
manual atomic.Bool
29+
}
30+
31+
// NewLogKnob creates a new LogKnob, with the provided environment variable
32+
// name and/or NetMap capability.
33+
func NewLogKnob(env, cap string) *LogKnob {
34+
if env == "" && cap == "" {
35+
panic("must provide either an environment variable or capability")
36+
}
37+
38+
lk := &LogKnob{
39+
capName: cap,
40+
}
41+
if env != "" {
42+
lk.env = envknob.RegisterBool(env)
43+
} else {
44+
lk.env = func() bool { return false }
45+
}
46+
return lk
47+
}
48+
49+
// Set will cause logs to be printed when called with Set(true). When called
50+
// with Set(false), logs will not be printed due to an earlier call of
51+
// Set(true), but may be printed due to either the envknob and/or capability of
52+
// this LogKnob.
53+
func (lk *LogKnob) Set(v bool) {
54+
lk.manual.Store(v)
55+
}
56+
57+
// NetMap is an interface for the parts of netmap.NetworkMap that we care
58+
// about; we use this rather than a concrete type to avoid a circular
59+
// dependency.
60+
type NetMap interface {
61+
SelfCapabilities() []string
62+
}
63+
64+
// UpdateFromNetMap will enable logging if the SelfNode in the provided NetMap
65+
// contains the capability provided for this LogKnob.
66+
func (lk *LogKnob) UpdateFromNetMap(nm NetMap) {
67+
if lk.capName == "" {
68+
return
69+
}
70+
71+
lk.cap.Store(slices.Contains(nm.SelfCapabilities(), lk.capName))
72+
}
73+
74+
// Do will call log with the provided format and arguments if any of the
75+
// configured methods for enabling logging are true.
76+
func (lk *LogKnob) Do(log logger.Logf, format string, args ...any) {
77+
if lk.shouldLog() {
78+
log(format, args...)
79+
}
80+
}
81+
82+
func (lk *LogKnob) shouldLog() bool {
83+
return lk.manual.Load() || lk.env() || lk.cap.Load()
84+
}

envknob/logknob/logknob_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
package logknob
5+
6+
import (
7+
"bytes"
8+
"fmt"
9+
"testing"
10+
11+
"tailscale.com/envknob"
12+
"tailscale.com/tailcfg"
13+
"tailscale.com/types/netmap"
14+
)
15+
16+
var testKnob = NewLogKnob(
17+
"TS_TEST_LOGKNOB",
18+
"https://tailscale.com/cap/testing",
19+
)
20+
21+
// Static type assertion for our interface type.
22+
var _ NetMap = &netmap.NetworkMap{}
23+
24+
func TestLogKnob(t *testing.T) {
25+
t.Run("Default", func(t *testing.T) {
26+
if testKnob.shouldLog() {
27+
t.Errorf("expected default shouldLog()=false")
28+
}
29+
assertNoLogs(t)
30+
})
31+
t.Run("Manual", func(t *testing.T) {
32+
t.Cleanup(func() { testKnob.Set(false) })
33+
34+
assertNoLogs(t)
35+
testKnob.Set(true)
36+
if !testKnob.shouldLog() {
37+
t.Errorf("expected shouldLog()=true")
38+
}
39+
assertLogs(t)
40+
})
41+
t.Run("Env", func(t *testing.T) {
42+
t.Cleanup(func() {
43+
envknob.Setenv("TS_TEST_LOGKNOB", "")
44+
})
45+
46+
assertNoLogs(t)
47+
if testKnob.shouldLog() {
48+
t.Errorf("expected default shouldLog()=false")
49+
}
50+
51+
envknob.Setenv("TS_TEST_LOGKNOB", "true")
52+
if !testKnob.shouldLog() {
53+
t.Errorf("expected shouldLog()=true")
54+
}
55+
assertLogs(t)
56+
})
57+
t.Run("NetMap", func(t *testing.T) {
58+
t.Cleanup(func() { testKnob.cap.Store(false) })
59+
60+
assertNoLogs(t)
61+
if testKnob.shouldLog() {
62+
t.Errorf("expected default shouldLog()=false")
63+
}
64+
65+
testKnob.UpdateFromNetMap(&netmap.NetworkMap{
66+
SelfNode: &tailcfg.Node{
67+
Capabilities: []string{
68+
"https://tailscale.com/cap/testing",
69+
},
70+
},
71+
})
72+
if !testKnob.shouldLog() {
73+
t.Errorf("expected shouldLog()=true")
74+
}
75+
assertLogs(t)
76+
})
77+
}
78+
79+
func assertLogs(t *testing.T) {
80+
var buf bytes.Buffer
81+
logf := func(format string, args ...any) {
82+
fmt.Fprintf(&buf, format, args...)
83+
}
84+
85+
testKnob.Do(logf, "hello %s", "world")
86+
const want = "hello world"
87+
if got := buf.String(); got != want {
88+
t.Errorf("got %q, want %q", got, want)
89+
}
90+
}
91+
92+
func assertNoLogs(t *testing.T) {
93+
var buf bytes.Buffer
94+
logf := func(format string, args ...any) {
95+
fmt.Fprintf(&buf, format, args...)
96+
}
97+
98+
testKnob.Do(logf, "hello %s", "world")
99+
if got := buf.String(); got != "" {
100+
t.Errorf("expected no logs, but got: %q", got)
101+
}
102+
}

types/netmap/netmap.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,17 @@ func (nm *NetworkMap) MagicDNSSuffix() string {
125125
return name
126126
}
127127

128+
// SelfCapabilities returns SelfNode.Capabilities if nm and nm.SelfNode are
129+
// non-nil. This is a method so we can use it in envknob/logknob without a
130+
// circular dependency.
131+
func (nm *NetworkMap) SelfCapabilities() []string {
132+
if nm == nil || nm.SelfNode == nil {
133+
return nil
134+
}
135+
136+
return nm.SelfNode.Capabilities
137+
}
138+
128139
func (nm *NetworkMap) String() string {
129140
return nm.Concise()
130141
}

0 commit comments

Comments
 (0)