mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Add ability to enable/disable logs by topic (#40126)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #40124 # Details Implements the proposal in https://docs.google.com/document/d/16qe6oVLKK25nA9GEIPR9Gw_IJ342_wlJRdnWEMmWdas/edit?tab=t.0#heading=h.nlw4agv1xs3g Allows doing e.g. ```go logger.WarnContext(logCtx, "The `team_id` param is deprecated, use `fleet_id` instead", "log_topic", "deprecated-field-names") ``` or ```go if logging.TopicEnabled("deprecated-api-params") { logging.WithLevel(ctx, slog.LevelWarn) logging.WithExtras( ctx, "deprecated_param", queryTagValue, "deprecation_warning", fmt.Sprintf("'%s' is deprecated, use '%s'", queryTagValue, renameTo), ) } ``` Topics can be disabled at the app level, and enabled/disabled at the command-line level. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [X] Added/updated automated tests - [X] QA'd all new/changed functionality manually No logs have this in prod yet, but I added some manually in a branch and verified that I could enable/disable them via CLI options and env vars, including enabling topics that were disabled on the server. Tested for both server and `fleetctl gitops`. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added per-topic logging control to enable or disable logging for specific topics via configuration and CLI flags. * Added context-aware logging methods (ErrorContext, WarnContext, InfoContext, DebugContext) to support contextual logging. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
f147a1a607
commit
421dc67e0c
15 changed files with 479 additions and 16 deletions
1
changes/40124-add-log-topics
Normal file
1
changes/40124-add-log-topics
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Added ability to enable/disable logs by topic
|
||||
|
|
@ -32,6 +32,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/httpsig"
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/pkg/scripts"
|
||||
"github.com/fleetdm/fleet/v4/pkg/str"
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/acl/activityacl"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
|
|
@ -67,6 +68,7 @@ import (
|
|||
nanomdm_pushsvc "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push/service"
|
||||
"github.com/fleetdm/fleet/v4/server/platform/endpointer"
|
||||
platform_http "github.com/fleetdm/fleet/v4/server/platform/http"
|
||||
platform_logging "github.com/fleetdm/fleet/v4/server/platform/logging"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/pubsub"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
|
|
@ -234,6 +236,22 @@ the way that the Fleet server works.
|
|||
|
||||
logger := initLogger(config, loggerProvider)
|
||||
|
||||
// If you want to disable any logs by default, this is where to do it.
|
||||
//
|
||||
// For example:
|
||||
// platform_logging.DisableTopic("deprecated-api-keys")
|
||||
|
||||
// Apply log topic overrides from config. Enables run first, then
|
||||
// disables, so disable wins on conflict.
|
||||
// Note that any topic not included in these lists will be considered
|
||||
// enabled if it's encountered in a log.
|
||||
for _, topic := range str.SplitAndTrim(config.Logging.EnableLogTopics, ",", true) {
|
||||
platform_logging.EnableTopic(topic)
|
||||
}
|
||||
for _, topic := range str.SplitAndTrim(config.Logging.DisableLogTopics, ",", true) {
|
||||
platform_logging.DisableTopic(topic)
|
||||
}
|
||||
|
||||
if dev_mode.IsEnabled {
|
||||
createTestBuckets(&config, logger)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
package fleetctl
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
import (
|
||||
"github.com/fleetdm/fleet/v4/pkg/str"
|
||||
"github.com/fleetdm/fleet/v4/server/platform/logging"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
outfileFlagName = "outfile"
|
||||
debugFlagName = "debug"
|
||||
fleetCertificateFlagName = "fleet-certificate"
|
||||
stdoutFlagName = "stdout"
|
||||
enableLogTopicsFlagName = "enable-log-topics"
|
||||
disableLogTopicsFlagName = "disable-log-topics"
|
||||
)
|
||||
|
||||
func outfileFlag() cli.Flag {
|
||||
|
|
@ -58,6 +64,41 @@ func getStdout(c *cli.Context) bool {
|
|||
return c.Bool(stdoutFlagName)
|
||||
}
|
||||
|
||||
func enableLogTopicsFlag() cli.Flag {
|
||||
return &cli.StringFlag{
|
||||
Name: enableLogTopicsFlagName,
|
||||
EnvVars: []string{"FLEET_ENABLE_LOG_TOPICS"},
|
||||
Usage: "Comma-separated log topics to enable",
|
||||
}
|
||||
}
|
||||
|
||||
func getEnabledLogTopics(c *cli.Context) string {
|
||||
return c.String(enableLogTopicsFlagName)
|
||||
}
|
||||
|
||||
func disableLogTopicsFlag() cli.Flag {
|
||||
return &cli.StringFlag{
|
||||
Name: disableLogTopicsFlagName,
|
||||
EnvVars: []string{"FLEET_DISABLE_LOG_TOPICS"},
|
||||
Usage: "Comma-separated log topics to disable",
|
||||
}
|
||||
}
|
||||
|
||||
func getDisabledLogTopics(c *cli.Context) string {
|
||||
return c.String(disableLogTopicsFlagName)
|
||||
}
|
||||
|
||||
// applyLogTopicFlags parses the enable/disable log topic flags and applies them.
|
||||
// Enables run first, then disables, so disable wins on conflict.
|
||||
func applyLogTopicFlags(c *cli.Context) {
|
||||
for _, topic := range str.SplitAndTrim(getEnabledLogTopics(c), ",", true) {
|
||||
logging.EnableTopic(topic)
|
||||
}
|
||||
for _, topic := range str.SplitAndTrim(getDisabledLogTopics(c), ",", true) {
|
||||
logging.DisableTopic(topic)
|
||||
}
|
||||
}
|
||||
|
||||
func byHostIdentifier() cli.Flag {
|
||||
return &cli.StringFlag{
|
||||
Name: "host",
|
||||
|
|
|
|||
|
|
@ -76,12 +76,17 @@ func gitopsCommand() *cli.Command {
|
|||
configFlag(),
|
||||
contextFlag(),
|
||||
debugFlag(),
|
||||
enableLogTopicsFlag(),
|
||||
disableLogTopicsFlag(),
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logf := func(format string, a ...interface{}) {
|
||||
_, _ = fmt.Fprintf(c.App.Writer, format, a...)
|
||||
}
|
||||
|
||||
// Apply log topic overrides from CLI flags.
|
||||
applyLogTopicFlags(c)
|
||||
|
||||
if len(c.Args().Slice()) != 0 {
|
||||
return errors.New("No positional arguments are allowed. To load multiple config files, use one -f flag per file.")
|
||||
}
|
||||
|
|
|
|||
15
pkg/str/str.go
Normal file
15
pkg/str/str.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package str
|
||||
|
||||
import "strings"
|
||||
|
||||
func SplitAndTrim(s string, delimiter string, removeEmpty bool) []string {
|
||||
parts := strings.Split(s, delimiter)
|
||||
cleaned := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if !removeEmpty || part != "" {
|
||||
cleaned = append(cleaned, part)
|
||||
}
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
88
pkg/str/str_test.go
Normal file
88
pkg/str/str_test.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package str
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSplitAndTrim(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
testString string
|
||||
delimiter string
|
||||
removeEmpty bool
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "basic comma split",
|
||||
testString: "a,b,c",
|
||||
delimiter: ",",
|
||||
removeEmpty: false,
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "trims whitespace",
|
||||
testString: " a , b , c ",
|
||||
delimiter: ",",
|
||||
removeEmpty: false,
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "keeps empty parts when removeEmpty is false",
|
||||
testString: "a,,b,,c",
|
||||
delimiter: ",",
|
||||
removeEmpty: false,
|
||||
expected: []string{"a", "", "b", "", "c"},
|
||||
},
|
||||
{
|
||||
name: "removes empty parts when removeEmpty is true",
|
||||
testString: "a,,b,,c",
|
||||
delimiter: ",",
|
||||
removeEmpty: true,
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "removes whitespace-only parts when removeEmpty is true",
|
||||
testString: "a, ,b, ,c",
|
||||
delimiter: ",",
|
||||
removeEmpty: true,
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
testString: "",
|
||||
delimiter: ",",
|
||||
removeEmpty: true,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "empty string without removeEmpty",
|
||||
testString: "",
|
||||
delimiter: ",",
|
||||
removeEmpty: false,
|
||||
expected: []string{""},
|
||||
},
|
||||
{
|
||||
name: "no delimiter found",
|
||||
testString: "abc",
|
||||
delimiter: ",",
|
||||
removeEmpty: false,
|
||||
expected: []string{"abc"},
|
||||
},
|
||||
{
|
||||
name: "multi-char delimiter",
|
||||
testString: "a::b::c",
|
||||
delimiter: "::",
|
||||
removeEmpty: false,
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := SplitAndTrim(tt.testString, tt.delimiter, tt.removeEmpty)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -276,7 +276,9 @@ type LoggingConfig struct {
|
|||
TracingType string `yaml:"tracing_type"`
|
||||
// OtelLogsEnabled enables exporting logs to an OpenTelemetry collector.
|
||||
// When enabled, logs are sent to both stderr and the OTLP endpoint.
|
||||
OtelLogsEnabled bool `yaml:"otel_logs_enabled"`
|
||||
OtelLogsEnabled bool `yaml:"otel_logs_enabled"`
|
||||
EnableLogTopics string `yaml:"enable_topics"`
|
||||
DisableLogTopics string `yaml:"disable_topics"`
|
||||
}
|
||||
|
||||
// ActivityConfig defines configs related to activities.
|
||||
|
|
@ -1322,6 +1324,10 @@ func (man Manager) addConfigs() {
|
|||
"Select the kind of tracing, defaults to OpenTelemetry, can also be elasticapm")
|
||||
man.addConfigBool("logging.otel_logs_enabled", false,
|
||||
"Enable exporting logs to an OpenTelemetry collector (requires tracing_enabled)")
|
||||
man.addConfigString("logging.enable_topics", "",
|
||||
"Comma-separated log topics to enable (overrides code defaults)")
|
||||
man.addConfigString("logging.disable_topics", "",
|
||||
"Comma-separated log topics to disable (overrides code defaults)")
|
||||
|
||||
// Email
|
||||
man.addConfigString("email.backend", "", "Provide the email backend type, acceptable values are currently \"ses\" and \"default\" or empty string which will default to SMTP")
|
||||
|
|
@ -1752,6 +1758,8 @@ func (man Manager) LoadConfig() FleetConfig {
|
|||
TracingEnabled: man.getConfigBool("logging.tracing_enabled"),
|
||||
TracingType: man.getConfigString("logging.tracing_type"),
|
||||
OtelLogsEnabled: man.getConfigBool("logging.otel_logs_enabled"),
|
||||
EnableLogTopics: man.getConfigString("logging.enable_topics"),
|
||||
DisableLogTopics: man.getConfigString("logging.disable_topics"),
|
||||
},
|
||||
Firehose: FirehoseConfig{
|
||||
Region: man.getConfigString("firehose.region"),
|
||||
|
|
|
|||
|
|
@ -106,5 +106,25 @@ func (a *Logger) SlogLogger() *slog.Logger {
|
|||
return a.logger
|
||||
}
|
||||
|
||||
// Wrap slog's ErrorContext method.
|
||||
func (a *Logger) ErrorContext(ctx context.Context, msg string, keyvals ...any) {
|
||||
a.logger.ErrorContext(ctx, msg, keyvals...)
|
||||
}
|
||||
|
||||
// Wrap slog's WarnContext method.
|
||||
func (a *Logger) WarnContext(ctx context.Context, msg string, keyvals ...any) {
|
||||
a.logger.WarnContext(ctx, msg, keyvals...)
|
||||
}
|
||||
|
||||
// Wrap slog's InfoContext method.
|
||||
func (a *Logger) InfoContext(ctx context.Context, msg string, keyvals ...any) {
|
||||
a.logger.InfoContext(ctx, msg, keyvals...)
|
||||
}
|
||||
|
||||
// Wrap slog's DebugContext method.
|
||||
func (a *Logger) DebugContext(ctx context.Context, msg string, keyvals ...any) {
|
||||
a.logger.DebugContext(ctx, msg, keyvals...)
|
||||
}
|
||||
|
||||
// Ensure Logger implements kitlog.Logger at compile time.
|
||||
var _ kitlog.Logger = (*Logger)(nil)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
|
|
@ -56,7 +57,6 @@ func TestKitlogAdapter(t *testing.T) {
|
|||
attrs := testutils.RecordAttrs(record)
|
||||
assert.Equal(t, "test-component", attrs["component"])
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestKitlogAdapterLevels(t *testing.T) {
|
||||
|
|
@ -105,3 +105,51 @@ func TestKitlogAdapterLevels(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKitlogSlogWrappers(t *testing.T) {
|
||||
handler := testutils.NewTestHandler()
|
||||
adapter := NewLogger(slog.New(handler))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
logFunc func(ctx context.Context, msg string, keyvals ...any)
|
||||
expectedLevel slog.Level
|
||||
}{
|
||||
{
|
||||
name: "error",
|
||||
logFunc: adapter.ErrorContext,
|
||||
expectedLevel: slog.LevelError,
|
||||
},
|
||||
{
|
||||
name: "warn",
|
||||
logFunc: adapter.WarnContext,
|
||||
expectedLevel: slog.LevelWarn,
|
||||
},
|
||||
{
|
||||
name: "info",
|
||||
logFunc: adapter.InfoContext,
|
||||
expectedLevel: slog.LevelInfo,
|
||||
},
|
||||
{
|
||||
name: "debug",
|
||||
logFunc: adapter.DebugContext,
|
||||
expectedLevel: slog.LevelDebug,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tc.logFunc(context.Background(), tc.name+" message", "key", "value")
|
||||
|
||||
record := handler.LastRecord()
|
||||
require.NotNil(t, record)
|
||||
assert.Equal(t, tc.name+" message", record.Message)
|
||||
assert.Equal(t, tc.expectedLevel, record.Level)
|
||||
|
||||
attrs := testutils.RecordAttrs(record)
|
||||
assert.Equal(t, "value", attrs["key"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@ func NewSlogLogger(opts Options) *slog.Logger {
|
|||
handler = NewMultiHandler(handler, otelHandler)
|
||||
}
|
||||
|
||||
// Wrap with topic filter as outermost handler so topic-disabled
|
||||
// messages are dropped before any other handler processes them.
|
||||
handler = NewTopicFilterHandler(handler)
|
||||
|
||||
return slog.New(handler)
|
||||
}
|
||||
|
||||
|
|
|
|||
64
server/platform/logging/topic_handler.go
Normal file
64
server/platform/logging/topic_handler.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
const logTopicAttrKey = "log_topic"
|
||||
|
||||
// TopicFilterHandler is a slog.Handler that filters log records based on
|
||||
// the log_topic found in the log record's attributes, or on the handler
|
||||
// itself if set using `WithAttrs`.
|
||||
type TopicFilterHandler struct {
|
||||
base slog.Handler
|
||||
topic string
|
||||
}
|
||||
|
||||
// NewTopicFilterHandler wraps base with topic-aware filtering.
|
||||
func NewTopicFilterHandler(base slog.Handler) *TopicFilterHandler {
|
||||
return &TopicFilterHandler{base: base}
|
||||
}
|
||||
|
||||
// Enabled reports whether the handler handles records at the given level.
|
||||
func (h *TopicFilterHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
return h.base.Enabled(ctx, level)
|
||||
}
|
||||
|
||||
// Handle processes the log record. It performs a defensive re-check of the
|
||||
// topic before delegating to the base handler.
|
||||
func (h *TopicFilterHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
topic := h.topic
|
||||
// Override any log_topic attribute on the handler with one from the record's attributes, if present.
|
||||
r.Attrs(func(a slog.Attr) bool {
|
||||
if a.Key == logTopicAttrKey {
|
||||
topic = a.Value.String()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
// Don't log if there's a disabled topic either on the handler or the record.
|
||||
if topic != "" && !TopicEnabled(topic) {
|
||||
return nil
|
||||
}
|
||||
return h.base.Handle(ctx, r)
|
||||
}
|
||||
|
||||
// WithAttrs returns a new TopicFilterHandler wrapping the base with added attributes.
|
||||
func (h *TopicFilterHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
// Check if the new attributes include a log_topic that should override the handler's current topic.
|
||||
for _, a := range attrs {
|
||||
if a.Key == logTopicAttrKey {
|
||||
return &TopicFilterHandler{base: h.base.WithAttrs(attrs), topic: a.Value.String()}
|
||||
}
|
||||
}
|
||||
return &TopicFilterHandler{base: h.base.WithAttrs(attrs), topic: h.topic}
|
||||
}
|
||||
|
||||
// WithGroup returns a new TopicFilterHandler wrapping the base with the given group.
|
||||
func (h *TopicFilterHandler) WithGroup(name string) slog.Handler {
|
||||
return &TopicFilterHandler{base: h.base.WithGroup(name), topic: h.topic}
|
||||
}
|
||||
|
||||
// Ensure TopicFilterHandler implements slog.Handler at compile time.
|
||||
var _ slog.Handler = (*TopicFilterHandler)(nil)
|
||||
85
server/platform/logging/topic_handler_test.go
Normal file
85
server/platform/logging/topic_handler_test.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTopicTestLogger(buf *bytes.Buffer) *slog.Logger {
|
||||
handler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelInfo})
|
||||
handler2 := NewTopicFilterHandler(handler)
|
||||
return slog.New(handler2)
|
||||
}
|
||||
|
||||
func TestTopicHandler_NoTopic(t *testing.T) {
|
||||
t.Cleanup(ResetTopics)
|
||||
var buf bytes.Buffer
|
||||
logger := newTopicTestLogger(&buf)
|
||||
|
||||
logger.InfoContext(context.Background(), "hello")
|
||||
assert.Contains(t, buf.String(), "hello")
|
||||
}
|
||||
|
||||
func TestTopicHandler_DisabledTopicByAttr(t *testing.T) {
|
||||
t.Cleanup(ResetTopics)
|
||||
var buf bytes.Buffer
|
||||
logger := newTopicTestLogger(&buf)
|
||||
|
||||
DisableTopic("my-topic")
|
||||
logger.InfoContext(context.Background(), "should not appear", "log_topic", "my-topic")
|
||||
assert.Empty(t, buf.String())
|
||||
}
|
||||
|
||||
func TestTopicHandler_DisabledTopicByWithAttr(t *testing.T) {
|
||||
t.Cleanup(ResetTopics)
|
||||
var buf bytes.Buffer
|
||||
logger := newTopicTestLogger(&buf)
|
||||
|
||||
DisableTopic("my-topic")
|
||||
logger = logger.With("log_topic", "my-topic")
|
||||
logger.InfoContext(context.Background(), "should not appear")
|
||||
assert.Empty(t, buf.String())
|
||||
|
||||
// Test overriding the handler topic with a different per-log topic.
|
||||
logger.InfoContext(context.Background(), "should appear", "log_topic", "other-topic")
|
||||
assert.Contains(t, buf.String(), "should appear")
|
||||
}
|
||||
|
||||
func TestTopicHandler_RespectsBaseLevel(t *testing.T) {
|
||||
t.Cleanup(ResetTopics)
|
||||
var buf bytes.Buffer
|
||||
// Base handler at Info level — Debug messages should be dropped regardless of topic.
|
||||
handler := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})
|
||||
logger := slog.New(NewTopicFilterHandler(handler))
|
||||
|
||||
logger.DebugContext(context.Background(), "debug message")
|
||||
assert.Empty(t, buf.String())
|
||||
}
|
||||
|
||||
func TestTopicHandler_WithAttrsPassesThrough(t *testing.T) {
|
||||
t.Cleanup(ResetTopics)
|
||||
var buf bytes.Buffer
|
||||
logger := newTopicTestLogger(&buf)
|
||||
|
||||
logger = logger.With("key", "value")
|
||||
logger.InfoContext(context.Background(), "with attrs")
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "with attrs")
|
||||
assert.Contains(t, output, "key=value")
|
||||
}
|
||||
|
||||
func TestTopicHandler_WithGroupPassesThrough(t *testing.T) {
|
||||
t.Cleanup(ResetTopics)
|
||||
var buf bytes.Buffer
|
||||
logger := newTopicTestLogger(&buf)
|
||||
|
||||
logger = logger.WithGroup("grp")
|
||||
logger.InfoContext(context.Background(), "with group", "k", "v")
|
||||
output := buf.String()
|
||||
require.Contains(t, output, "grp.k=v")
|
||||
}
|
||||
42
server/platform/logging/topics.go
Normal file
42
server/platform/logging/topics.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// disabledTopics tracks which topics have been explicitly disabled.
|
||||
// Topics are enabled by default — only topics in this map are disabled.
|
||||
var (
|
||||
disabledTopics = make(map[string]bool)
|
||||
disabledTopicsMu sync.RWMutex
|
||||
)
|
||||
|
||||
// EnableTopic marks a topic as enabled (removes it from the disabled set).
|
||||
func EnableTopic(name string) {
|
||||
disabledTopicsMu.Lock()
|
||||
delete(disabledTopics, name)
|
||||
disabledTopicsMu.Unlock()
|
||||
}
|
||||
|
||||
// DisableTopic marks a topic as disabled.
|
||||
func DisableTopic(name string) {
|
||||
disabledTopicsMu.Lock()
|
||||
disabledTopics[name] = true
|
||||
disabledTopicsMu.Unlock()
|
||||
}
|
||||
|
||||
// TopicEnabled returns true unless the topic has been explicitly disabled.
|
||||
func TopicEnabled(name string) bool {
|
||||
disabledTopicsMu.RLock()
|
||||
disabled := disabledTopics[name]
|
||||
disabledTopicsMu.RUnlock()
|
||||
return !disabled
|
||||
}
|
||||
|
||||
// ResetTopics clears all disabled topics, re-enabling everything.
|
||||
// This is intended for use in tests to ensure isolation.
|
||||
func ResetTopics() {
|
||||
disabledTopicsMu.Lock()
|
||||
disabledTopics = make(map[string]bool)
|
||||
disabledTopicsMu.Unlock()
|
||||
}
|
||||
35
server/platform/logging/topics_test.go
Normal file
35
server/platform/logging/topics_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTopicEnabledByDefault(t *testing.T) {
|
||||
t.Cleanup(ResetTopics)
|
||||
assert.True(t, TopicEnabled("unknown-topic"))
|
||||
}
|
||||
|
||||
func TestDisableTopic(t *testing.T) {
|
||||
t.Cleanup(ResetTopics)
|
||||
DisableTopic("my-topic")
|
||||
assert.False(t, TopicEnabled("my-topic"))
|
||||
}
|
||||
|
||||
func TestEnableTopicReenables(t *testing.T) {
|
||||
t.Cleanup(ResetTopics)
|
||||
DisableTopic("my-topic")
|
||||
assert.False(t, TopicEnabled("my-topic"))
|
||||
EnableTopic("my-topic")
|
||||
assert.True(t, TopicEnabled("my-topic"))
|
||||
}
|
||||
|
||||
func TestResetTopics(t *testing.T) {
|
||||
t.Cleanup(ResetTopics)
|
||||
DisableTopic("a")
|
||||
DisableTopic("b")
|
||||
ResetTopics()
|
||||
assert.True(t, TopicEnabled("a"))
|
||||
assert.True(t, TopicEnabled("b"))
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/str"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
||||
|
|
@ -2552,7 +2553,7 @@ func directIngestMunkiInfo(ctx context.Context, logger *slog.Logger, host *fleet
|
|||
}
|
||||
|
||||
errors, warnings := rows[0]["errors"], rows[0]["warnings"]
|
||||
errList, warnList := splitCleanSemicolonSeparated(errors), splitCleanSemicolonSeparated(warnings)
|
||||
errList, warnList := str.SplitAndTrim(errors, ";", true), str.SplitAndTrim(warnings, ";", true)
|
||||
return ds.SetOrUpdateMunkiInfo(ctx, host.ID, rows[0]["version"], errList, warnList)
|
||||
}
|
||||
|
||||
|
|
@ -3199,18 +3200,6 @@ func GetDetailQueries(
|
|||
return generatedMap
|
||||
}
|
||||
|
||||
func splitCleanSemicolonSeparated(s string) []string {
|
||||
parts := strings.Split(s, ";")
|
||||
cleaned := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part != "" {
|
||||
cleaned = append(cleaned, part)
|
||||
}
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func buildConfigProfilesWindowsQuery(
|
||||
ctx context.Context,
|
||||
logger *slog.Logger,
|
||||
|
|
|
|||
Loading…
Reference in a new issue