Skip to content

Commit 2271844

Browse files
CelianREokye
andauthored
[anomalydetection] Smart adaptive log sampling logic 3/4 (#53431)
### What does this PR do? > [!NOTE] > [Feature documentation](https://datadoghq.atlassian.net/wiki/x/EgK5nQE). This links anomaly detection events to adaptive sampling to have different profiles given severity state. ```yaml logs_config: experimental_adaptive_sampling: enabled: true rate_limit: 1 burst_size: 100 # New config smart_severity_profiles: enabled: true medium: rate_limit: 1 burst_size: 1000 high: rate_limit: 100 burst_size: 1000 ``` Adds: - Logic to `preprocessor/sampler.go` such that it reads the `severityevents` state and apply the specific sampling profile given the severity - `smart_adaptive_log_sampling_nix_test.go` e2e test to test this feature (1. check high sampling in low state, 2. check low sampling in high state) ### Motivation ### Describe how you validated your changes 1. Config smart severity profiles + adaptive sampling 2. The low profile must be the first one applied 3. When we have anomalies (medium / high severity), the other profiles must be set. We can set the burst size to a high value and check if bursts of logs are sent or not There is an e2e tests for this. ### Additional Notes Passthrough / cooldown are left for a [future PR](#53432). <img width="573" height="800" alt="Screenshot 2026-07-09 at 09 41 50" src="https://github.com/user-attachments/assets/343e59d4-ab53-4977-8f81-071dbc2cbe6d" /> [AAD-4]: https://datadoghq.atlassian.net/browse/AAD-4?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ Co-authored-by: eman.okyere <eman.okyere@datadoghq.com>
1 parent af6d610 commit 2271844

10 files changed

Lines changed: 798 additions & 27 deletions

File tree

pkg/logs/internal/decoder/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ go_library(
3131
"@com_github_datadog_datadog_agent_pkg_logs_util_testutils//:__subpackages__",
3232
],
3333
deps = [
34+
"//comp/anomalydetection/severityevents/def",
3435
"//comp/core/telemetry/def",
3536
"//comp/logs-library/metrics",
3637
"//comp/logs/agent/config",
38+
"//comp/logs/severityprovider/def",
3739
"//pkg/config/setup",
3840
"//pkg/config/structure",
3941
"//pkg/logs/internal/decoder/preprocessor",
@@ -65,6 +67,7 @@ dd_agent_go_test(
6567
],
6668
embed = [":decoder"],
6769
deps = [
70+
"//comp/anomalydetection/severityevents/def",
6871
"//comp/logs/agent/config",
6972
"//pkg/config/mock",
7073
"//pkg/config/model",

pkg/logs/internal/decoder/decoder.go

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"regexp"
1010
"time"
1111

12+
severityeventsdef "github.com/DataDog/datadog-agent/comp/anomalydetection/severityevents/def"
1213
"github.com/DataDog/datadog-agent/comp/logs/agent/config"
14+
severityprovider "github.com/DataDog/datadog-agent/comp/logs/severityprovider/def"
1315
pkgconfigsetup "github.com/DataDog/datadog-agent/pkg/config/setup"
1416
"github.com/DataDog/datadog-agent/pkg/config/structure"
1517
"github.com/DataDog/datadog-agent/pkg/logs/internal/decoder/preprocessor"
@@ -189,6 +191,47 @@ func resolveNoisyLogDetectionEnabled(sourceNoisyLogDetection *bool) bool {
189191

190192
const disabledSourcesConfigKey = "logs_config.experimental_adaptive_sampling.disabled_sources"
191193

194+
const (
195+
smartSeverityProfilesEnabledConfigKey = "logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled"
196+
smartSeverityProfilesMediumRateLimitConfigKey = "logs_config.experimental_adaptive_sampling.smart_severity_profiles.medium.rate_limit"
197+
smartSeverityProfilesMediumBurstSizeConfigKey = "logs_config.experimental_adaptive_sampling.smart_severity_profiles.medium.burst_size"
198+
smartSeverityProfilesHighRateLimitConfigKey = "logs_config.experimental_adaptive_sampling.smart_severity_profiles.high.rate_limit"
199+
smartSeverityProfilesHighBurstSizeConfigKey = "logs_config.experimental_adaptive_sampling.smart_severity_profiles.high.burst_size"
200+
)
201+
202+
// resolveSmartSeverityProfiles builds the Low/Medium/High profile triple. Each field of
203+
// Medium/High cascades independently from the level below when left unconfigured (Low ->
204+
// Medium -> High), so no combination of partially-configured fields can leave a higher
205+
// severity level less permissive than the one below it.
206+
func resolveSmartSeverityProfiles(low preprocessor.SamplerProfile) [severityeventsdef.NumSeverityLevels]preprocessor.SamplerProfile {
207+
cfg := pkgconfigsetup.Datadog()
208+
209+
profiles := [severityeventsdef.NumSeverityLevels]preprocessor.SamplerProfile{
210+
severityeventsdef.SeverityLow: low,
211+
severityeventsdef.SeverityMedium: low,
212+
severityeventsdef.SeverityHigh: low,
213+
}
214+
215+
if cfg.IsConfigured(smartSeverityProfilesMediumRateLimitConfigKey) {
216+
profiles[severityeventsdef.SeverityMedium].RateLimit = cfg.GetFloat64(smartSeverityProfilesMediumRateLimitConfigKey)
217+
}
218+
if cfg.IsConfigured(smartSeverityProfilesMediumBurstSizeConfigKey) {
219+
profiles[severityeventsdef.SeverityMedium].BurstSize = clampBurstSize(cfg.GetFloat64(smartSeverityProfilesMediumBurstSizeConfigKey))
220+
}
221+
222+
// High starts from Medium's already-resolved profile, then applies its own
223+
// overrides per field.
224+
profiles[severityeventsdef.SeverityHigh] = profiles[severityeventsdef.SeverityMedium]
225+
if cfg.IsConfigured(smartSeverityProfilesHighRateLimitConfigKey) {
226+
profiles[severityeventsdef.SeverityHigh].RateLimit = cfg.GetFloat64(smartSeverityProfilesHighRateLimitConfigKey)
227+
}
228+
if cfg.IsConfigured(smartSeverityProfilesHighBurstSizeConfigKey) {
229+
profiles[severityeventsdef.SeverityHigh].BurstSize = clampBurstSize(cfg.GetFloat64(smartSeverityProfilesHighBurstSizeConfigKey))
230+
}
231+
232+
return profiles
233+
}
234+
192235
func newDisabledSet() map[string]struct{} {
193236
entries := pkgconfigsetup.Datadog().GetStringSlice(disabledSourcesConfigKey)
194237
m := make(map[string]struct{}, len(entries))
@@ -276,7 +319,15 @@ func resolveAdaptiveSamplerConfig(sourceAdaptiveSampling *config.SourceAdaptiveS
276319
}
277320
}
278321

279-
return validateAdaptiveSamplerConfig(c)
322+
c = validateAdaptiveSamplerConfig(c)
323+
324+
c.SmartSeverityProfilesEnabled = pkgconfigsetup.Datadog().GetBool(smartSeverityProfilesEnabledConfigKey)
325+
if c.SmartSeverityProfilesEnabled {
326+
c.Profiles = resolveSmartSeverityProfiles(preprocessor.SamplerProfile{RateLimit: c.RateLimit, BurstSize: c.BurstSize})
327+
c.SeverityProvider = severityprovider.Current
328+
}
329+
330+
return c
280331
}
281332

282333
func resolveNoisyLogDetectionConfig(sourceAdaptiveSampling *config.SourceAdaptiveSamplingOptions, tok *preprocessor.Tokenizer) preprocessor.AdaptiveSamplerConfig {
@@ -425,13 +476,19 @@ func validateAdaptiveSamplerConfig(c preprocessor.AdaptiveSamplerConfig) preproc
425476
c.MaxPatterns = 1
426477
}
427478

428-
if c.BurstSize <= 0 {
429-
c.BurstSize = 1
430-
}
479+
c.BurstSize = clampBurstSize(c.BurstSize)
431480

432481
return c
433482
}
434483

484+
// clampBurstSize floors burstSize at 1, avoiding negative starting credits.
485+
func clampBurstSize(burstSize float64) float64 {
486+
if burstSize <= 0 {
487+
return 1
488+
}
489+
return burstSize
490+
}
491+
435492
func getLegacyAutoMultilineHandler(outputFn func(*message.Message), multiLinePattern *regexp.Regexp, maxContentSize int, source *sources.ReplaceableSource, detectedPattern *DetectedPattern, tailerInfo *status.InfoRegistry) LineHandler {
436493

437494
if multiLinePattern != nil {

pkg/logs/internal/decoder/decoder_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212
"testing"
1313

14+
severityeventsdef "github.com/DataDog/datadog-agent/comp/anomalydetection/severityevents/def"
1415
"github.com/DataDog/datadog-agent/comp/logs/agent/config"
1516
configmock "github.com/DataDog/datadog-agent/pkg/config/mock"
1617
pkgconfigmodel "github.com/DataDog/datadog-agent/pkg/config/model"
@@ -741,6 +742,101 @@ func TestResolveAdaptiveSamplerConfig(t *testing.T) {
741742
assert.Equal(t, 0.8, got.MatchThreshold)
742743
assert.True(t, got.DetectionOnly)
743744
})
745+
746+
t.Run("smart severity profiles enabled: unconfigured medium/high fall back to the base (low) values", func(t *testing.T) {
747+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled", true, pkgconfigmodel.SourceAgentRuntime)
748+
defer mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled", false, pkgconfigmodel.SourceAgentRuntime)
749+
750+
got := resolveAdaptiveSamplerConfig(nil, preprocessor.NewTokenizer(0))
751+
752+
assert.True(t, got.SmartSeverityProfilesEnabled)
753+
low := preprocessor.SamplerProfile{RateLimit: got.RateLimit, BurstSize: got.BurstSize}
754+
assert.Equal(t, low, got.Profiles[severityeventsdef.SeverityLow])
755+
assert.Equal(t, low, got.Profiles[severityeventsdef.SeverityMedium])
756+
assert.Equal(t, low, got.Profiles[severityeventsdef.SeverityHigh])
757+
})
758+
759+
// Must run before any subtest that Sets (and later resets) medium.burst_size:
760+
// mockConfig has no true "unset", so a prior Set — even one whose deferred
761+
// cleanup restores the default value — permanently marks the key as
762+
// configured for every subtest that follows.
763+
t.Run("smart severity profiles enabled: high cascades a partially-configured medium field-by-field", func(t *testing.T) {
764+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled", true, pkgconfigmodel.SourceAgentRuntime)
765+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.medium.rate_limit", 5.0, pkgconfigmodel.SourceAgentRuntime)
766+
defer func() {
767+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled", false, pkgconfigmodel.SourceAgentRuntime)
768+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.medium.rate_limit", 1.0, pkgconfigmodel.SourceAgentRuntime)
769+
}()
770+
771+
got := resolveAdaptiveSamplerConfig(nil, preprocessor.NewTokenizer(0))
772+
773+
want := preprocessor.SamplerProfile{RateLimit: 5.0, BurstSize: got.BurstSize}
774+
assert.Equal(t, want, got.Profiles[severityeventsdef.SeverityMedium])
775+
assert.Equal(t, want, got.Profiles[severityeventsdef.SeverityHigh], "high's unconfigured rate_limit must cascade from medium, not revert to low, so high is never less permissive than medium")
776+
})
777+
778+
t.Run("smart severity profiles enabled: unset high falls back to configured medium", func(t *testing.T) {
779+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled", true, pkgconfigmodel.SourceAgentRuntime)
780+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.medium.rate_limit", 5.0, pkgconfigmodel.SourceAgentRuntime)
781+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.medium.burst_size", 200.0, pkgconfigmodel.SourceAgentRuntime)
782+
defer func() {
783+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled", false, pkgconfigmodel.SourceAgentRuntime)
784+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.medium.rate_limit", 1.0, pkgconfigmodel.SourceAgentRuntime)
785+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.medium.burst_size", 1000.0, pkgconfigmodel.SourceAgentRuntime)
786+
}()
787+
788+
got := resolveAdaptiveSamplerConfig(nil, preprocessor.NewTokenizer(0))
789+
790+
assert.Equal(t, preprocessor.SamplerProfile{RateLimit: 5.0, BurstSize: 200.0}, got.Profiles[severityeventsdef.SeverityMedium])
791+
assert.Equal(t, got.Profiles[severityeventsdef.SeverityMedium], got.Profiles[severityeventsdef.SeverityHigh])
792+
})
793+
794+
t.Run("smart severity profiles enabled: explicit medium/high overrides win", func(t *testing.T) {
795+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled", true, pkgconfigmodel.SourceAgentRuntime)
796+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.medium.rate_limit", 5.0, pkgconfigmodel.SourceAgentRuntime)
797+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.medium.burst_size", 200.0, pkgconfigmodel.SourceAgentRuntime)
798+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.high.rate_limit", 20.0, pkgconfigmodel.SourceAgentRuntime)
799+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.high.burst_size", 500.0, pkgconfigmodel.SourceAgentRuntime)
800+
defer func() {
801+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled", false, pkgconfigmodel.SourceAgentRuntime)
802+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.medium.rate_limit", 1.0, pkgconfigmodel.SourceAgentRuntime)
803+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.medium.burst_size", 1000.0, pkgconfigmodel.SourceAgentRuntime)
804+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.high.rate_limit", 1.0, pkgconfigmodel.SourceAgentRuntime)
805+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.high.burst_size", 1000.0, pkgconfigmodel.SourceAgentRuntime)
806+
}()
807+
808+
got := resolveAdaptiveSamplerConfig(nil, preprocessor.NewTokenizer(0))
809+
810+
assert.Equal(t, preprocessor.SamplerProfile{RateLimit: 2.5, BurstSize: 50.0}, got.Profiles[severityeventsdef.SeverityLow])
811+
assert.Equal(t, preprocessor.SamplerProfile{RateLimit: 5.0, BurstSize: 200.0}, got.Profiles[severityeventsdef.SeverityMedium])
812+
assert.Equal(t, preprocessor.SamplerProfile{RateLimit: 20.0, BurstSize: 500.0}, got.Profiles[severityeventsdef.SeverityHigh])
813+
})
814+
815+
t.Run("smart severity profiles enabled: a non-positive configured burst size is clamped to 1", func(t *testing.T) {
816+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled", true, pkgconfigmodel.SourceAgentRuntime)
817+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.high.burst_size", 0.0, pkgconfigmodel.SourceAgentRuntime)
818+
defer func() {
819+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled", false, pkgconfigmodel.SourceAgentRuntime)
820+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.high.burst_size", 1000.0, pkgconfigmodel.SourceAgentRuntime)
821+
}()
822+
823+
got := resolveAdaptiveSamplerConfig(nil, preprocessor.NewTokenizer(0))
824+
825+
assert.Equal(t, preprocessor.SamplerProfile{RateLimit: 1.0, BurstSize: 1.0}, got.Profiles[severityeventsdef.SeverityHigh])
826+
})
827+
828+
t.Run("smart severity profiles enabled: a negative configured burst size is clamped to 1", func(t *testing.T) {
829+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled", true, pkgconfigmodel.SourceAgentRuntime)
830+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.high.burst_size", -50.0, pkgconfigmodel.SourceAgentRuntime)
831+
defer func() {
832+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.enabled", false, pkgconfigmodel.SourceAgentRuntime)
833+
mockConfig.Set("logs_config.experimental_adaptive_sampling.smart_severity_profiles.high.burst_size", 1000.0, pkgconfigmodel.SourceAgentRuntime)
834+
}()
835+
836+
got := resolveAdaptiveSamplerConfig(nil, preprocessor.NewTokenizer(0))
837+
838+
assert.Equal(t, preprocessor.SamplerProfile{RateLimit: 1.0, BurstSize: 1.0}, got.Profiles[severityeventsdef.SeverityHigh])
839+
})
744840
}
745841

746842
func TestDecoderWithDockerJSONPartialLineDetectionOnlyMarksOversizedLogicalLineTruncated(t *testing.T) {

pkg/logs/internal/decoder/preprocessor/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ go_library(
3636
"@com_github_datadog_datadog_agent_pkg_logs_util_testutils//:__subpackages__",
3737
],
3838
deps = [
39+
"//comp/anomalydetection/severityevents/def",
3940
"//comp/core/telemetry/def",
4041
"//comp/core/telemetry/impl",
4142
"//comp/logs-library/metrics",
@@ -63,6 +64,7 @@ dd_agent_go_test(
6364
"pattern_table_test.go",
6465
"preprocessor_test.go",
6566
"sampler_benchmark_test.go",
67+
"sampler_smart_severity_profile_test.go",
6668
"sampler_test.go",
6769
"stack_trace_aggregator_test.go",
6870
"timestamp_detector_test.go",
@@ -73,6 +75,7 @@ dd_agent_go_test(
7375
],
7476
embed = [":preprocessor"],
7577
deps = [
78+
"//comp/anomalydetection/severityevents/def",
7679
"//comp/logs-library/metrics",
7780
"//comp/logs/agent/config",
7881
"//pkg/config/mock",

pkg/logs/internal/decoder/preprocessor/sampler.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strconv"
1313
"time"
1414

15+
severityeventsdef "github.com/DataDog/datadog-agent/comp/anomalydetection/severityevents/def"
1516
telemetryimpl "github.com/DataDog/datadog-agent/comp/core/telemetry/impl"
1617
"github.com/DataDog/datadog-agent/pkg/logs/message"
1718
)
@@ -117,6 +118,23 @@ type AdaptiveSamplerConfig struct {
117118
// message through without rate-limiting. Using a closure lets the check track
118119
// ReplaceableSource swaps and future Remote Config updates.
119120
IsSourceDisabled func() bool
121+
// SmartSeverityProfilesEnabled switches RateLimit/BurstSize based on the level
122+
// read from SeverityProvider (see Profiles). Profiles[SeverityLow] must match
123+
// RateLimit/BurstSize above.
124+
SmartSeverityProfilesEnabled bool
125+
// Profiles holds the RateLimit/BurstSize pair per SeverityLevel.
126+
// Only consulted when SmartSeverityProfilesEnabled is true.
127+
Profiles [severityeventsdef.NumSeverityLevels]SamplerProfile
128+
// SeverityProvider returns the current anomaly-detection severity level, or false
129+
// when no reader is registered yet. Only consulted when SmartSeverityProfilesEnabled
130+
// is true. Left nil in tests that don't exercise smart severity profiles.
131+
SeverityProvider func() (severityeventsdef.SeverityLevel, bool)
132+
}
133+
134+
// SamplerProfile is a RateLimit/BurstSize pair for one SeverityLevel.
135+
type SamplerProfile struct {
136+
RateLimit float64
137+
BurstSize float64
120138
}
121139

122140
// AdaptiveSamplerFilter matches messages by raw-content regex, structural sample,
@@ -148,6 +166,13 @@ type AdaptiveSampler struct {
148166
source string // used as a telemetry tag
149167
now func() time.Time
150168
baseBytesEstimate int
169+
170+
// appliedLevel tracks the last severity profile applied by
171+
// applyProfileIfChanged. appliedLevelInitialized stays false until a real
172+
// reader is registered, so the sampler does not treat the no-reader case as
173+
// an implicit Low profile.
174+
appliedLevel severityeventsdef.SeverityLevel
175+
appliedLevelInitialized bool
151176
}
152177

153178
// NewAdaptiveSampler creates a new AdaptiveSampler.
@@ -219,6 +244,42 @@ func (s *AdaptiveSampler) appendPatternHashTagIfEnabled(msg *message.Message, to
219244
}
220245
}
221246

247+
// applyProfileIfChanged switches RateLimit/BurstSize to the currently published
248+
// level, when SmartSeverityProfilesEnabled is set. Escalation grants every
249+
// pattern a fresh burst immediately. The first available Medium/High level is
250+
// also treated as an escalation from the base Low profile. De-escalation leaves
251+
// credits untouched, letting the refill-time clamp in processMatchedEntry
252+
// shrink them naturally.
253+
func (s *AdaptiveSampler) applyProfileIfChanged() {
254+
if s.config.SeverityProvider == nil {
255+
return
256+
}
257+
level, ok := s.config.SeverityProvider()
258+
if !ok {
259+
return
260+
}
261+
if s.appliedLevelInitialized && level == s.appliedLevel {
262+
return
263+
}
264+
265+
wasInitialized := s.appliedLevelInitialized
266+
previousLevel := s.appliedLevel
267+
profile := s.config.Profiles[level]
268+
escalation := (wasInitialized && level > previousLevel) ||
269+
(!wasInitialized && level > severityeventsdef.SeverityLow)
270+
s.config.RateLimit = profile.RateLimit
271+
s.config.BurstSize = profile.BurstSize
272+
273+
if escalation {
274+
for i := range s.entries {
275+
s.entries[i].credits = s.config.BurstSize
276+
}
277+
}
278+
279+
s.appliedLevel = level
280+
s.appliedLevelInitialized = true
281+
}
282+
222283
// Process applies credit-based rate limiting to the message.
223284
// Returns the message if allowed, nil if dropped.
224285
func (s *AdaptiveSampler) Process(msg *message.Message, tokens []Token) *message.Message {
@@ -227,6 +288,9 @@ func (s *AdaptiveSampler) Process(msg *message.Message, tokens []Token) *message
227288
if !msg.HasContent() {
228289
return msg
229290
}
291+
if s.config.SmartSeverityProfilesEnabled {
292+
s.applyProfileIfChanged()
293+
}
230294
if s.config.IsSourceDisabled != nil && s.config.IsSourceDisabled() {
231295
return msg
232296
}

0 commit comments

Comments
 (0)