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 <andrew@du.nham.ca> Change-Id: Id66c608d4e488bfd4eaa5e867a8d9289686748bemain
parent
3b39ca9017
commit
8ed4fd1dbc
@ -0,0 +1,84 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package logknob provides a helpful wrapper that allows enabling logging
|
||||
// based on either an envknob or other methods of enablement.
|
||||
package logknob |
||||
|
||||
import ( |
||||
"sync/atomic" |
||||
|
||||
"golang.org/x/exp/slices" |
||||
"tailscale.com/envknob" |
||||
"tailscale.com/types/logger" |
||||
) |
||||
|
||||
// TODO(andrew-d): should we have a package-global registry of logknobs? It
|
||||
// would allow us to update from a netmap in a central location, which might be
|
||||
// reason enough to do it...
|
||||
|
||||
// LogKnob allows configuring verbose logging, with multiple ways to enable. It
|
||||
// supports enabling logging via envknob, via atomic boolean (for use in e.g.
|
||||
// c2n log level changes), and via capabilities from a NetMap (so users can
|
||||
// enable logging via the ACL JSON).
|
||||
type LogKnob struct { |
||||
capName string |
||||
cap atomic.Bool |
||||
env func() bool |
||||
manual atomic.Bool |
||||
} |
||||
|
||||
// NewLogKnob creates a new LogKnob, with the provided environment variable
|
||||
// name and/or NetMap capability.
|
||||
func NewLogKnob(env, cap string) *LogKnob { |
||||
if env == "" && cap == "" { |
||||
panic("must provide either an environment variable or capability") |
||||
} |
||||
|
||||
lk := &LogKnob{ |
||||
capName: cap, |
||||
} |
||||
if env != "" { |
||||
lk.env = envknob.RegisterBool(env) |
||||
} else { |
||||
lk.env = func() bool { return false } |
||||
} |
||||
return lk |
||||
} |
||||
|
||||
// Set will cause logs to be printed when called with Set(true). When called
|
||||
// with Set(false), logs will not be printed due to an earlier call of
|
||||
// Set(true), but may be printed due to either the envknob and/or capability of
|
||||
// this LogKnob.
|
||||
func (lk *LogKnob) Set(v bool) { |
||||
lk.manual.Store(v) |
||||
} |
||||
|
||||
// NetMap is an interface for the parts of netmap.NetworkMap that we care
|
||||
// about; we use this rather than a concrete type to avoid a circular
|
||||
// dependency.
|
||||
type NetMap interface { |
||||
SelfCapabilities() []string |
||||
} |
||||
|
||||
// UpdateFromNetMap will enable logging if the SelfNode in the provided NetMap
|
||||
// contains the capability provided for this LogKnob.
|
||||
func (lk *LogKnob) UpdateFromNetMap(nm NetMap) { |
||||
if lk.capName == "" { |
||||
return |
||||
} |
||||
|
||||
lk.cap.Store(slices.Contains(nm.SelfCapabilities(), lk.capName)) |
||||
} |
||||
|
||||
// Do will call log with the provided format and arguments if any of the
|
||||
// configured methods for enabling logging are true.
|
||||
func (lk *LogKnob) Do(log logger.Logf, format string, args ...any) { |
||||
if lk.shouldLog() { |
||||
log(format, args...) |
||||
} |
||||
} |
||||
|
||||
func (lk *LogKnob) shouldLog() bool { |
||||
return lk.manual.Load() || lk.env() || lk.cap.Load() |
||||
} |
||||
@ -0,0 +1,102 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package logknob |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"testing" |
||||
|
||||
"tailscale.com/envknob" |
||||
"tailscale.com/tailcfg" |
||||
"tailscale.com/types/netmap" |
||||
) |
||||
|
||||
var testKnob = NewLogKnob( |
||||
"TS_TEST_LOGKNOB", |
||||
"https://tailscale.com/cap/testing", |
||||
) |
||||
|
||||
// Static type assertion for our interface type.
|
||||
var _ NetMap = &netmap.NetworkMap{} |
||||
|
||||
func TestLogKnob(t *testing.T) { |
||||
t.Run("Default", func(t *testing.T) { |
||||
if testKnob.shouldLog() { |
||||
t.Errorf("expected default shouldLog()=false") |
||||
} |
||||
assertNoLogs(t) |
||||
}) |
||||
t.Run("Manual", func(t *testing.T) { |
||||
t.Cleanup(func() { testKnob.Set(false) }) |
||||
|
||||
assertNoLogs(t) |
||||
testKnob.Set(true) |
||||
if !testKnob.shouldLog() { |
||||
t.Errorf("expected shouldLog()=true") |
||||
} |
||||
assertLogs(t) |
||||
}) |
||||
t.Run("Env", func(t *testing.T) { |
||||
t.Cleanup(func() { |
||||
envknob.Setenv("TS_TEST_LOGKNOB", "") |
||||
}) |
||||
|
||||
assertNoLogs(t) |
||||
if testKnob.shouldLog() { |
||||
t.Errorf("expected default shouldLog()=false") |
||||
} |
||||
|
||||
envknob.Setenv("TS_TEST_LOGKNOB", "true") |
||||
if !testKnob.shouldLog() { |
||||
t.Errorf("expected shouldLog()=true") |
||||
} |
||||
assertLogs(t) |
||||
}) |
||||
t.Run("NetMap", func(t *testing.T) { |
||||
t.Cleanup(func() { testKnob.cap.Store(false) }) |
||||
|
||||
assertNoLogs(t) |
||||
if testKnob.shouldLog() { |
||||
t.Errorf("expected default shouldLog()=false") |
||||
} |
||||
|
||||
testKnob.UpdateFromNetMap(&netmap.NetworkMap{ |
||||
SelfNode: &tailcfg.Node{ |
||||
Capabilities: []string{ |
||||
"https://tailscale.com/cap/testing", |
||||
}, |
||||
}, |
||||
}) |
||||
if !testKnob.shouldLog() { |
||||
t.Errorf("expected shouldLog()=true") |
||||
} |
||||
assertLogs(t) |
||||
}) |
||||
} |
||||
|
||||
func assertLogs(t *testing.T) { |
||||
var buf bytes.Buffer |
||||
logf := func(format string, args ...any) { |
||||
fmt.Fprintf(&buf, format, args...) |
||||
} |
||||
|
||||
testKnob.Do(logf, "hello %s", "world") |
||||
const want = "hello world" |
||||
if got := buf.String(); got != want { |
||||
t.Errorf("got %q, want %q", got, want) |
||||
} |
||||
} |
||||
|
||||
func assertNoLogs(t *testing.T) { |
||||
var buf bytes.Buffer |
||||
logf := func(format string, args ...any) { |
||||
fmt.Fprintf(&buf, format, args...) |
||||
} |
||||
|
||||
testKnob.Do(logf, "hello %s", "world") |
||||
if got := buf.String(); got != "" { |
||||
t.Errorf("expected no logs, but got: %q", got) |
||||
} |
||||
} |
||||
Loading…
Reference in new issue