From 421dc67e0c12a8903b80f1cfd9eb91f9d65310df Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Fri, 20 Feb 2026 17:22:50 -0600 Subject: [PATCH] Add ability to enable/disable logs by topic (#40126) **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`. ## 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. --- changes/40124-add-log-topics | 1 + cmd/fleet/serve.go | 18 ++++ cmd/fleetctl/fleetctl/flags.go | 43 ++++++++- cmd/fleetctl/fleetctl/gitops.go | 5 ++ pkg/str/str.go | 15 ++++ pkg/str/str_test.go | 88 +++++++++++++++++++ server/config/config.go | 10 ++- server/platform/logging/kitlog_adapter.go | 20 +++++ .../platform/logging/kitlog_adapter_test.go | 50 ++++++++++- server/platform/logging/logging.go | 4 + server/platform/logging/topic_handler.go | 64 ++++++++++++++ server/platform/logging/topic_handler_test.go | 85 ++++++++++++++++++ server/platform/logging/topics.go | 42 +++++++++ server/platform/logging/topics_test.go | 35 ++++++++ server/service/osquery_utils/queries.go | 15 +--- 15 files changed, 479 insertions(+), 16 deletions(-) create mode 100644 changes/40124-add-log-topics create mode 100644 pkg/str/str.go create mode 100644 pkg/str/str_test.go create mode 100644 server/platform/logging/topic_handler.go create mode 100644 server/platform/logging/topic_handler_test.go create mode 100644 server/platform/logging/topics.go create mode 100644 server/platform/logging/topics_test.go diff --git a/changes/40124-add-log-topics b/changes/40124-add-log-topics new file mode 100644 index 0000000000..d41b082cee --- /dev/null +++ b/changes/40124-add-log-topics @@ -0,0 +1 @@ +- Added ability to enable/disable logs by topic diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 8d5f964508..4b33550d35 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -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) } diff --git a/cmd/fleetctl/fleetctl/flags.go b/cmd/fleetctl/fleetctl/flags.go index 847cce2952..b45b4026a8 100644 --- a/cmd/fleetctl/fleetctl/flags.go +++ b/cmd/fleetctl/fleetctl/flags.go @@ -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", diff --git a/cmd/fleetctl/fleetctl/gitops.go b/cmd/fleetctl/fleetctl/gitops.go index ac6fcece76..ecb6c8f400 100644 --- a/cmd/fleetctl/fleetctl/gitops.go +++ b/cmd/fleetctl/fleetctl/gitops.go @@ -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.") } diff --git a/pkg/str/str.go b/pkg/str/str.go new file mode 100644 index 0000000000..5fa9b27506 --- /dev/null +++ b/pkg/str/str.go @@ -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 +} diff --git a/pkg/str/str_test.go b/pkg/str/str_test.go new file mode 100644 index 0000000000..b2df000ba9 --- /dev/null +++ b/pkg/str/str_test.go @@ -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) + }) + } +} diff --git a/server/config/config.go b/server/config/config.go index 1aee5d7e6c..46a1251231 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -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"), diff --git a/server/platform/logging/kitlog_adapter.go b/server/platform/logging/kitlog_adapter.go index fa8e62aa11..5b52c58641 100644 --- a/server/platform/logging/kitlog_adapter.go +++ b/server/platform/logging/kitlog_adapter.go @@ -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) diff --git a/server/platform/logging/kitlog_adapter_test.go b/server/platform/logging/kitlog_adapter_test.go index 20a57f2966..e70cdf8b62 100644 --- a/server/platform/logging/kitlog_adapter_test.go +++ b/server/platform/logging/kitlog_adapter_test.go @@ -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"]) + }) + } +} diff --git a/server/platform/logging/logging.go b/server/platform/logging/logging.go index 5a7bb93680..365ebb3ae4 100644 --- a/server/platform/logging/logging.go +++ b/server/platform/logging/logging.go @@ -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) } diff --git a/server/platform/logging/topic_handler.go b/server/platform/logging/topic_handler.go new file mode 100644 index 0000000000..00347fdc32 --- /dev/null +++ b/server/platform/logging/topic_handler.go @@ -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) diff --git a/server/platform/logging/topic_handler_test.go b/server/platform/logging/topic_handler_test.go new file mode 100644 index 0000000000..8ea02efe44 --- /dev/null +++ b/server/platform/logging/topic_handler_test.go @@ -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") +} diff --git a/server/platform/logging/topics.go b/server/platform/logging/topics.go new file mode 100644 index 0000000000..97695d5794 --- /dev/null +++ b/server/platform/logging/topics.go @@ -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() +} diff --git a/server/platform/logging/topics_test.go b/server/platform/logging/topics_test.go new file mode 100644 index 0000000000..4efa58ac80 --- /dev/null +++ b/server/platform/logging/topics_test.go @@ -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")) +} diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index 6de6bdf5c5..1d01214c09 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -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,