Skip to content

Add ability to enable/disable logs by topic - #40126

Merged
sgress454 merged 12 commits into
mainfrom
sgress454/log-topics
Feb 20, 2026
Merged

Add ability to enable/disable logs by topic#40126
sgress454 merged 12 commits into
mainfrom
sgress454/log-topics

Conversation

@sgress454

@sgress454 sgress454 commented Feb 19, 2026

Copy link
Copy Markdown
Contributor

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.

logger.WarnContext(logCtx, "The `team_id` param is deprecated, use `fleet_id` instead", "log_topic", "deprecated-field-names")

or

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.

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

Testing

  • Added/updated automated tests
  • 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.

Comment on lines +109 to +128
// 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...)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added these wrappers so that we can start using slog semantics before the full migration, e.g.

svc.logger.ErrorContext(ctx, "some log")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modeled this on the Otel wrapper. It just skips logs if there's a disabled topic in the context.

@codecov

codecov Bot commented Feb 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.33%. Comparing base (c303f7f) to head (62894b5).
⚠️ Report is 69 commits behind head on main.

Files with missing lines Patch % Lines
cmd/fleet/serve.go 0.00% 4 Missing ⚠️
cmd/fleetctl/fleetctl/flags.go 78.94% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #40126      +/-   ##
==========================================
+ Coverage   66.28%   66.33%   +0.04%     
==========================================
  Files        2446     2452       +6     
  Lines      195912   196624     +712     
  Branches     8574     8574              
==========================================
+ Hits       129869   130424     +555     
- Misses      54279    54388     +109     
- Partials    11764    11812      +48     
Flag Coverage Δ
backend 68.13% <90.90%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sgress454 sgress454 mentioned this pull request Feb 19, 2026
3 tasks
@getvictor

Copy link
Copy Markdown
Member

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR implements per-topic logging control functionality. It introduces a topic state management system in the logging package that tracks disabled topics via a thread-safe map and provides enable/disable APIs. A new TopicFilterHandler wraps the logging handler chain to filter out records with disabled topics. Configuration support is added via logging.enable_topics and logging.disable_topics keys, with CLI flag overrides in fleetctl. Server-side initialization applies these overrides during startup. The kitlog adapter is extended with context-aware logging methods (ErrorContext, WarnContext, InfoContext, DebugContext). Topics are enabled by default unless explicitly disabled.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Add ability to enable/disable logs by topic' accurately summarizes the main feature added—a mechanism for controlling log output by topic.
Description check ✅ Passed The PR description is well-structured and complete, including related issue reference, implementation details, example usage, checklist items completed, and testing verification.
Linked Issues check ✅ Passed All coding objectives from issue #40124 are met: topic-based log filtering [topic_handler.go, topics.go], flow control via TopicEnabled [topics.go], config and CLI/env var overrides [config.go, flags.go, serve.go], and disabled topics are ignored [topic_handler.go].
Out of Scope Changes check ✅ Passed All changes directly support the log topics feature: new topic filtering/gating logic, config additions, CLI flags, slog adapter methods for context logging, tests, and a changes file entry—no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sgress454/log-topics

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
server/platform/logging/topic_handler.go (1)

23-28: Redundant if/return pattern — simplify to return h.base.Enabled(ctx, level).

♻️ Proposed fix
 func (h *TopicFilterHandler) Enabled(ctx context.Context, level slog.Level) bool {
-	if !h.base.Enabled(ctx, level) {
-		return false
-	}
-	return true
+	return h.base.Enabled(ctx, level)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/platform/logging/topic_handler.go` around lines 23 - 28, The Enabled
method on TopicFilterHandler uses a redundant if/return pattern; replace the
body of TopicFilterHandler.Enabled so it directly returns the result of
h.base.Enabled(ctx, level) (i.e., change the implementation of Enabled to simply
"return h.base.Enabled(ctx, level)") to simplify the logic and remove the
unnecessary conditional.
server/platform/logging/topics.go (2)

47-50: ResetTopics is test-only but exported from the main package.

The comment explicitly says "intended for use in tests to ensure isolation," yet it's a regular exported symbol visible to any consumer. Exposing it in production code risks accidental misuse and clutters the package's public API. Consider moving it to a export_test.go or testing_helpers_test.go file within the same package, or gating it under a testing package guard.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/platform/logging/topics.go` around lines 47 - 50, ResetTopics is
exported but intended only for tests; make it test-only by moving the
ResetTopics implementation into a test file (e.g., export_test.go or
testing_helpers_test.go) in the same package or change its visibility there
(e.g., make it unexported) so production consumers can't call it; update any
test callers to import/use the relocated ResetTopics (or the new unexported
helper within the package tests) and remove the exported ResetTopics from
topics.go (which contains disabledTopicsMu and disabledTopics) so the public API
no longer exposes this test-only helper.

9-12: map[string]bool — use map[string]struct{} for a set.

The map only ever stores true; map[string]struct{} is the idiomatic Go representation for a set and avoids the wasted bool byte per entry.

♻️ Proposed refactor
 var (
-	disabledTopics   = make(map[string]bool)
+	disabledTopics   = make(map[string]struct{})
 	disabledTopicsMu sync.RWMutex
 )

 func DisableTopic(name string) {
 	disabledTopicsMu.Lock()
-	disabledTopics[name] = true
+	disabledTopics[name] = struct{}{}
 	disabledTopicsMu.Unlock()
 }

 func TopicEnabled(name string) bool {
 	disabledTopicsMu.RLock()
-	disabled := disabledTopics[name]
+	_, disabled := disabledTopics[name]
 	disabledTopicsMu.RUnlock()
 	return !disabled
 }

 func ResetTopics() {
 	disabledTopicsMu.Lock()
-	disabledTopics = make(map[string]bool)
+	disabledTopics = make(map[string]struct{})
 	disabledTopicsMu.Unlock()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/platform/logging/topics.go` around lines 9 - 12, Replace the
boolean-set implementation with an empty-struct set: change the declaration of
disabledTopics from map[string]bool to map[string]struct{} (and its initializer
to make(map[string]struct{})), update places that add entries to use
disabledTopics[key] = struct{}{} instead of true, check membership with the
comma-ok pattern (_, ok := disabledTopics[key]) or by checking presence, and use
delete(disabledTopics, key) for removals; keep disabledTopicsMu as-is for
synchronization.
cmd/fleetctl/fleetctl/flags.go (1)

103-116: Consolidate parseLogTopicsList with existing parseLogTopics function.

Both cmd/fleetctl/fleetctl/flags.go and cmd/fleet/serve.go contain identical implementations of comma-separated string parsing for log topics. Move the shared logic to server/platform/logging package (which both files already import) to eliminate duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/fleetctl/fleetctl/flags.go` around lines 103 - 116, There are duplicate
comma-separated topic parsers (parseLogTopicsList in flags.go and parseLogTopics
in serve.go); factor the shared logic into the server/platform/logging package
(add an exported function, e.g. ParseLogTopics or ParseTopics) and remove the
local parseLogTopicsList and parseLogTopics implementations; update callers in
cmd/fleetctl/fleetctl/flags.go and cmd/fleet/serve.go to call the new
server/platform/logging.ParseLogTopics and adjust imports accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/platform/logging/kitlog_adapter_test.go`:
- Around line 109-155: The subtests run in parallel but share the single handler
and adapter declared at the top, causing races; fix TestKitlogSlogWrappers by
creating a fresh test handler and logger inside each subtest (move the
testutils.NewTestHandler() and NewLogger(slog.New(...)) calls into the t.Run
closure before tc.logFunc is invoked) so tc.logFunc, handler.LastRecord() and
testutils.RecordAttrs(record) operate on per-subtest instances and avoid
concurrent mutation.

In `@server/platform/logging/topic_handler.go`:
- Around line 10-12: The doc comment for TopicFilterHandler incorrectly states
it filters based on the context; update it to describe that filtering is
attribute-based by reading the "log_topic" attribute from the slog.Record rather
than from context.Context. Mention TopicFilterHandler and that it implements
slog.Handler, and explicitly state that records are dropped when their
slog.Record "log_topic" attribute matches a disabled topic.
- Around line 47-50: The TopicFilterHandler currently ignores topics provided
via logger.With because WithAttrs only delegates to base.WithAttrs and the
Handle method only checks r.Attrs(); update WithAttrs to extract any "log_topic"
attr from the incoming attrs and return a TopicFilterHandler that stores that
captured topic (e.g., a topic string or disabled flag) alongside base:
TopicFilterHandler{base: h.base.WithAttrs(attrs), capturedTopic: "..."} so that
Handle (in TopicFilterHandler.Handle) short-circuits filtering by checking both
r.Attrs() and the stored capturedTopic from the handler before deciding to pass
or drop the record; ensure you reference TopicFilterHandler.WithAttrs,
TopicFilterHandler.Handle, base, r.Attrs(), and the "log_topic" name when
implementing the change.

---

Nitpick comments:
In `@cmd/fleetctl/fleetctl/flags.go`:
- Around line 103-116: There are duplicate comma-separated topic parsers
(parseLogTopicsList in flags.go and parseLogTopics in serve.go); factor the
shared logic into the server/platform/logging package (add an exported function,
e.g. ParseLogTopics or ParseTopics) and remove the local parseLogTopicsList and
parseLogTopics implementations; update callers in cmd/fleetctl/fleetctl/flags.go
and cmd/fleet/serve.go to call the new server/platform/logging.ParseLogTopics
and adjust imports accordingly.

In `@server/platform/logging/topic_handler.go`:
- Around line 23-28: The Enabled method on TopicFilterHandler uses a redundant
if/return pattern; replace the body of TopicFilterHandler.Enabled so it directly
returns the result of h.base.Enabled(ctx, level) (i.e., change the
implementation of Enabled to simply "return h.base.Enabled(ctx, level)") to
simplify the logic and remove the unnecessary conditional.

In `@server/platform/logging/topics.go`:
- Around line 47-50: ResetTopics is exported but intended only for tests; make
it test-only by moving the ResetTopics implementation into a test file (e.g.,
export_test.go or testing_helpers_test.go) in the same package or change its
visibility there (e.g., make it unexported) so production consumers can't call
it; update any test callers to import/use the relocated ResetTopics (or the new
unexported helper within the package tests) and remove the exported ResetTopics
from topics.go (which contains disabledTopicsMu and disabledTopics) so the
public API no longer exposes this test-only helper.
- Around line 9-12: Replace the boolean-set implementation with an empty-struct
set: change the declaration of disabledTopics from map[string]bool to
map[string]struct{} (and its initializer to make(map[string]struct{})), update
places that add entries to use disabledTopics[key] = struct{}{} instead of true,
check membership with the comma-ok pattern (_, ok := disabledTopics[key]) or by
checking presence, and use delete(disabledTopics, key) for removals; keep
disabledTopicsMu as-is for synchronization.

Comment thread server/platform/logging/kitlog_adapter_test.go
Comment thread server/platform/logging/topic_handler.go Outdated
Comment thread server/platform/logging/topic_handler.go

@iansltx iansltx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant to flush this buffer but then got pulled into another thing. Going to finish reviewing now, sorry.

Comment thread cmd/fleetctl/fleetctl/flags.go Outdated
Comment thread cmd/fleetctl/fleetctl/flags.go Outdated
Comment thread server/platform/logging/topics.go Outdated
Comment thread cmd/fleet/serve.go Outdated
}

// parseLogTopics splits a comma-separated string into trimmed, non-empty topic names.
func parseLogTopics(s string) []string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're adding this twice, but have something extremely similar in splitCleanSemicolonSeparated, which is used once, so would be easy enough to add a delimiter option there and go from three functions to one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm down, but I don't know where to put a utility like this. Currently we have some in /server/fleet/utils.go which seems bad from a modularity standpoint, even if all the places that would use it currently import the fleet package anyway. So I could go with a new /pkg/utils or put it in /server/platform/utils. Any preferences? cc: @getvictor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pkg/str so we don't wind up with the utils junk drawer disease?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use strings.Split(s, delim) here?
I'm ok with pkg/str

@sgress454 sgress454 Feb 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use strings.Split(s, delim) here?

Alas we want to trim as well, and remove empty values. I'll add a generic "split string by delimiter and trim" function to /pkg/str and use it in all three places.

Comment thread server/platform/logging/topic_handler.go
@iansltx

iansltx commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

@sgress454 actually this seems like it's WIP due to architectural/edge case limitations. Ping me again when this is ready for another review if you like, though @getvictor is going to have a bit more context on this particular thing.

@getvictor getvictor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Please fix lint issues in topic_handler.go

Comment thread cmd/fleet/serve.go Outdated
}

// parseLogTopics splits a comma-separated string into trimmed, non-empty topic names.
func parseLogTopics(s string) []string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use strings.Split(s, delim) here?
I'm ok with pkg/str

Comment on lines -2555 to +2556
errList, warnList := splitCleanSemicolonSeparated(errors), splitCleanSemicolonSeparated(warnings)
errList, warnList := str.SplitAndTrim(errors, ";", true), str.SplitAndTrim(warnings, ";", true)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no test for the directIngestMunkiInfo function this is in, but I at least had two bots independently verify that the logic was equivalent.

@sgress454
sgress454 merged commit 421dc67 into main Feb 20, 2026
50 checks passed
@sgress454
sgress454 deleted the sgress454/log-topics branch February 20, 2026 23:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement ability to enable/disable logs by topic

3 participants