tailscale/util/syspolicy/internal/metrics/metrics_test.go
Nick Khyl aeb15dea30 util/syspolicy/source: add package for reading policy settings from external stores
We add package defining interfaces for policy stores, enabling creation of policy sources
and reading settings from them. It includes a Windows-specific PlatformPolicyStore for GP and MDM
policies stored in the Registry, and an in-memory TestStore for testing purposes.

We also include an internal package that tracks and reports policy usage metrics when a policy setting
is read from a store. Initially, it will be used only on Windows and Android, as macOS, iOS, and tvOS
report their own metrics. However, we plan to use it across all platforms eventually.

Updates #12687

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-09-03 14:51:14 -05:00

424 lines
12 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package metrics
import (
"errors"
"testing"
"tailscale.com/types/lazy"
"tailscale.com/util/clientmetric"
"tailscale.com/util/syspolicy/internal"
"tailscale.com/util/syspolicy/setting"
)
func TestSettingMetricNames(t *testing.T) {
tests := []struct {
name string
key setting.Key
scope setting.Scope
suffix string
typ clientmetric.Type
osOverride string
wantMetricName string
}{
{
name: "windows-device-no-suffix",
key: "AdminConsole",
scope: setting.DeviceSetting,
suffix: "",
typ: clientmetric.TypeCounter,
osOverride: "windows",
wantMetricName: "windows_syspolicy_AdminConsole",
},
{
name: "windows-user-no-suffix",
key: "AdminConsole",
scope: setting.UserSetting,
suffix: "",
typ: clientmetric.TypeCounter,
osOverride: "windows",
wantMetricName: "windows_syspolicy_AdminConsole_user",
},
{
name: "windows-profile-no-suffix",
key: "AdminConsole",
scope: setting.ProfileSetting,
suffix: "",
typ: clientmetric.TypeCounter,
osOverride: "windows",
wantMetricName: "windows_syspolicy_AdminConsole_profile",
},
{
name: "windows-profile-err",
key: "AdminConsole",
scope: setting.ProfileSetting,
suffix: "error",
typ: clientmetric.TypeCounter,
osOverride: "windows",
wantMetricName: "windows_syspolicy_AdminConsole_profile_error",
},
{
name: "android-device-no-suffix",
key: "AdminConsole",
scope: setting.DeviceSetting,
suffix: "",
typ: clientmetric.TypeCounter,
osOverride: "android",
wantMetricName: "android_syspolicy_AdminConsole",
},
{
name: "key-path",
key: "category/subcategory/setting",
scope: setting.DeviceSetting,
suffix: "",
typ: clientmetric.TypeCounter,
osOverride: "fakeos",
wantMetricName: "fakeos_syspolicy_category_subcategory_setting",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
internal.OSForTesting.SetForTest(t, tt.osOverride, nil)
metric, ok := newSettingMetric(tt.key, tt.scope, tt.suffix, tt.typ).(*funcMetric)
if !ok {
t.Fatal("metric is not a funcMetric")
}
if metric.name != tt.wantMetricName {
t.Errorf("got %q, want %q", metric.name, tt.wantMetricName)
}
})
}
}
func TestScopeMetrics(t *testing.T) {
tests := []struct {
name string
scope setting.Scope
osOverride string
wantHasAnyName string
wantNumErroredName string
wantHasAnyType clientmetric.Type
wantNumErroredType clientmetric.Type
}{
{
name: "windows-device",
scope: setting.DeviceSetting,
osOverride: "windows",
wantHasAnyName: "windows_syspolicy_any",
wantHasAnyType: clientmetric.TypeGauge,
wantNumErroredName: "windows_syspolicy_errors",
wantNumErroredType: clientmetric.TypeCounter,
},
{
name: "windows-profile",
scope: setting.ProfileSetting,
osOverride: "windows",
wantHasAnyName: "windows_syspolicy_profile_any",
wantHasAnyType: clientmetric.TypeGauge,
wantNumErroredName: "windows_syspolicy_profile_errors",
wantNumErroredType: clientmetric.TypeCounter,
},
{
name: "windows-user",
scope: setting.UserSetting,
osOverride: "windows",
wantHasAnyName: "windows_syspolicy_user_any",
wantHasAnyType: clientmetric.TypeGauge,
wantNumErroredName: "windows_syspolicy_user_errors",
wantNumErroredType: clientmetric.TypeCounter,
},
{
name: "android-device",
scope: setting.DeviceSetting,
osOverride: "android",
wantHasAnyName: "android_syspolicy_any",
wantHasAnyType: clientmetric.TypeGauge,
wantNumErroredName: "android_syspolicy_errors",
wantNumErroredType: clientmetric.TypeCounter,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
internal.OSForTesting.SetForTest(t, tt.osOverride, nil)
metrics := newScopeMetrics(tt.scope)
hasAny, ok := metrics.hasAny.(*funcMetric)
if !ok {
t.Fatal("hasAny is not a funcMetric")
}
numErrored, ok := metrics.numErrored.(*funcMetric)
if !ok {
t.Fatal("numErrored is not a funcMetric")
}
if hasAny.name != tt.wantHasAnyName {
t.Errorf("hasAny.Name: got %q, want %q", hasAny.name, tt.wantHasAnyName)
}
if hasAny.typ != tt.wantHasAnyType {
t.Errorf("hasAny.Type: got %q, want %q", hasAny.typ, tt.wantHasAnyType)
}
if numErrored.name != tt.wantNumErroredName {
t.Errorf("numErrored.Name: got %q, want %q", numErrored.name, tt.wantNumErroredName)
}
if numErrored.typ != tt.wantNumErroredType {
t.Errorf("hasAny.Type: got %q, want %q", numErrored.typ, tt.wantNumErroredType)
}
})
}
}
type testSettingDetails struct {
definition *setting.Definition
origin *setting.Origin
value any
err error
}
func TestReportMetrics(t *testing.T) {
tests := []struct {
name string
osOverride string
useMetrics bool
settings []testSettingDetails
wantMetrics []TestState
wantResetMetrics []TestState
}{
{
name: "none",
osOverride: "windows",
settings: []testSettingDetails{},
wantMetrics: []TestState{},
},
{
name: "single-value",
osOverride: "windows",
settings: []testSettingDetails{
{
definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.DeviceScope),
value: 42,
},
},
wantMetrics: []TestState{
{"windows_syspolicy_any", 1},
{"windows_syspolicy_TestSetting01", 1},
},
wantResetMetrics: []TestState{
{"windows_syspolicy_any", 0},
{"windows_syspolicy_TestSetting01", 0},
},
},
{
name: "single-error",
osOverride: "windows",
settings: []testSettingDetails{
{
definition: setting.NewDefinition("TestSetting02", setting.DeviceSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.DeviceScope),
err: errors.New("bang!"),
},
},
wantMetrics: []TestState{
{"windows_syspolicy_errors", 1},
{"windows_syspolicy_TestSetting02_error", 1},
},
wantResetMetrics: []TestState{
{"windows_syspolicy_errors", 1},
{"windows_syspolicy_TestSetting02_error", 0},
},
},
{
name: "value-and-error",
osOverride: "windows",
settings: []testSettingDetails{
{
definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.DeviceScope),
value: 42,
},
{
definition: setting.NewDefinition("TestSetting02", setting.DeviceSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.DeviceScope),
err: errors.New("bang!"),
},
},
wantMetrics: []TestState{
{"windows_syspolicy_any", 1},
{"windows_syspolicy_errors", 1},
{"windows_syspolicy_TestSetting01", 1},
{"windows_syspolicy_TestSetting02_error", 1},
},
wantResetMetrics: []TestState{
{"windows_syspolicy_any", 0},
{"windows_syspolicy_errors", 1},
{"windows_syspolicy_TestSetting01", 0},
{"windows_syspolicy_TestSetting02_error", 0},
},
},
{
name: "two-values",
osOverride: "windows",
settings: []testSettingDetails{
{
definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.DeviceScope),
value: 42,
},
{
definition: setting.NewDefinition("TestSetting02", setting.DeviceSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.DeviceScope),
value: 17,
},
},
wantMetrics: []TestState{
{"windows_syspolicy_any", 1},
{"windows_syspolicy_TestSetting01", 1},
{"windows_syspolicy_TestSetting02", 1},
},
wantResetMetrics: []TestState{
{"windows_syspolicy_any", 0},
{"windows_syspolicy_TestSetting01", 0},
{"windows_syspolicy_TestSetting02", 0},
},
},
{
name: "two-errors",
osOverride: "windows",
settings: []testSettingDetails{
{
definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.DeviceScope),
err: errors.New("bang!"),
},
{
definition: setting.NewDefinition("TestSetting02", setting.DeviceSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.DeviceScope),
err: errors.New("bang!"),
},
},
wantMetrics: []TestState{
{"windows_syspolicy_errors", 2},
{"windows_syspolicy_TestSetting01_error", 1},
{"windows_syspolicy_TestSetting02_error", 1},
},
wantResetMetrics: []TestState{
{"windows_syspolicy_errors", 2},
{"windows_syspolicy_TestSetting01_error", 0},
{"windows_syspolicy_TestSetting02_error", 0},
},
},
{
name: "multi-scope",
osOverride: "windows",
settings: []testSettingDetails{
{
definition: setting.NewDefinition("TestSetting01", setting.ProfileSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.DeviceScope),
value: 42,
},
{
definition: setting.NewDefinition("TestSetting02", setting.ProfileSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.CurrentProfileScope),
err: errors.New("bang!"),
},
{
definition: setting.NewDefinition("TestSetting03", setting.UserSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.CurrentUserScope),
value: 17,
},
},
wantMetrics: []TestState{
{"windows_syspolicy_any", 1},
{"windows_syspolicy_profile_errors", 1},
{"windows_syspolicy_user_any", 1},
{"windows_syspolicy_TestSetting01", 1},
{"windows_syspolicy_TestSetting02_profile_error", 1},
{"windows_syspolicy_TestSetting03_user", 1},
},
wantResetMetrics: []TestState{
{"windows_syspolicy_any", 0},
{"windows_syspolicy_profile_errors", 1},
{"windows_syspolicy_user_any", 0},
{"windows_syspolicy_TestSetting01", 0},
{"windows_syspolicy_TestSetting02_profile_error", 0},
{"windows_syspolicy_TestSetting03_user", 0},
},
},
{
name: "report-metrics-on-android",
osOverride: "android",
settings: []testSettingDetails{
{
definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.DeviceScope),
value: 42,
},
},
wantMetrics: []TestState{
{"android_syspolicy_any", 1},
{"android_syspolicy_TestSetting01", 1},
},
wantResetMetrics: []TestState{
{"android_syspolicy_any", 0},
{"android_syspolicy_TestSetting01", 0},
},
},
{
name: "do-not-report-metrics-on-macos",
osOverride: "macos",
settings: []testSettingDetails{
{
definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.DeviceScope),
value: 42,
},
},
wantMetrics: []TestState{}, // none reported
},
{
name: "do-not-report-metrics-on-ios",
osOverride: "ios",
settings: []testSettingDetails{
{
definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
origin: setting.NewOrigin(setting.DeviceScope),
value: 42,
},
},
wantMetrics: []TestState{}, // none reported
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Reset the lazy value so it'll be re-evaluated with the osOverride.
lazyReportMetrics = lazy.SyncValue[bool]{}
t.Cleanup(func() {
// Also reset it during the cleanup.
lazyReportMetrics = lazy.SyncValue[bool]{}
})
internal.OSForTesting.SetForTest(t, tt.osOverride, nil)
h := NewTestHandler(t)
SetHooksForTest(t, h.AddMetric, h.SetMetric)
for _, s := range tt.settings {
if s.err != nil {
ReportError(s.origin, s.definition, s.err)
} else {
ReportConfigured(s.origin, s.definition, s.value)
}
}
h.MustEqual(tt.wantMetrics...)
for _, s := range tt.settings {
Reset(s.origin)
ReportNotConfigured(s.origin, s.definition)
}
h.MustEqual(tt.wantResetMetrics...)
})
}
}