Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix log level detection #12651

Merged
merged 26 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a9ad857
Fix log level detection
shantanualsi Apr 17, 2024
a1d643d
Small bugfix
shantanualsi Apr 17, 2024
7d70360
Add support for trace and fatal detection
shantanualsi Apr 18, 2024
cbdc4c7
Merge branch 'main' into fix-log-level-detection
shantanualsi Apr 18, 2024
8d2ce13
Merge branch 'main' into fix-log-level-detection
shantanualsi Apr 19, 2024
00397ee
Support unknown log level and add some tests
shantanualsi Apr 19, 2024
147233a
Add support for LEVEL for discover levels
shantanualsi Apr 19, 2024
5d33553
Fix tests
shantanualsi Apr 19, 2024
bc62249
Merge branch 'main' into fix-log-level-detection
shantanualsi Apr 19, 2024
9cd72f9
Merge branch 'main' into fix-log-level-detection
shantanualsi Apr 19, 2024
441586b
Merge branch 'main' into fix-log-level-detection
shantanualsi Apr 22, 2024
979244b
Add method to parse both logfmt and json to detect level
shantanualsi Apr 22, 2024
207c6b7
Lint
shantanualsi Apr 22, 2024
be71206
Use alternative parsers to discover levels
shantanualsi Apr 22, 2024
86f1d22
Merge branch 'main' into fix-log-level-detection
shantanualsi Apr 23, 2024
a16b375
Optimize - don't run entire logfmt if = is not present in the log line
shantanualsi Apr 23, 2024
b01580e
Merge branch 'main' into fix-log-level-detection
shantanualsi Apr 24, 2024
4c8196e
Change detected level label
shantanualsi Apr 24, 2024
5a608a9
Merge branch 'main' into fix-log-level-detection
shantanualsi Apr 24, 2024
acd7f1f
Merge branch 'main' into fix-log-level-detection
shantanualsi Apr 25, 2024
f0f7bf8
Fix allowed labels and detected_level label
shantanualsi Apr 25, 2024
6da3509
Fix docs
shantanualsi Apr 25, 2024
8fa6d6c
Merge branch 'main' into fix-log-level-detection
shantanualsi Apr 29, 2024
9b1b91c
Add detected_level from label or existing structured metadata
shantanualsi May 6, 2024
7795d81
Merge branch 'main' into fix-log-level-detection
shantanualsi May 6, 2024
bae53e1
Do not populate detected_level when level is unknown
shantanualsi May 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/sources/shared/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2925,8 +2925,10 @@ The `limits_config` block configures global and per-tenant limits in Loki. The v
[discover_service_name: <list of strings> | default = [service app application name app_kubernetes_io_name container container_name component workload job]]

# Discover and add log levels during ingestion, if not present already. Levels
# would be added to Structured Metadata with name 'level' and one of the values
# from 'debug', 'info', 'warn', 'error', 'critical', 'fatal'.
# would be added to Structured Metadata with name
# level/LEVEL/Level/Severity/severity/SEVERITY/lvl/LVL/Lvl (case-sensitive) and
# one of the values from 'trace', 'debug', 'info', 'warn', 'error', 'critical',
# 'fatal' (case insensitive).
# CLI flag: -validation.discover-log-levels
[discover_log_levels: <boolean> | default = true]

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ require (
github.com/IBM/go-sdk-core/v5 v5.13.1
github.com/IBM/ibm-cos-sdk-go v1.10.0
github.com/axiomhq/hyperloglog v0.0.0-20240124082744-24bca3a5b39b
github.com/buger/jsonparser v1.1.1
github.com/d4l3k/messagediff v1.2.1
github.com/dolthub/swiss v0.2.1
github.com/efficientgo/core v1.0.0-rc.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@ github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b h1:6+ZFm0flnudZzdSE0JxlhR2hKnGPcNB35BjQf4RYQDY=
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/caddyserver/caddy v1.0.4/go.mod h1:uruyfVsyMcDb3IOzSKsi1x0wOjy1my/PxOSTcD+24jM=
Expand Down
161 changes: 103 additions & 58 deletions pkg/distributor/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import (
"time"
"unicode"

"github.com/buger/jsonparser"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/go-logfmt/logfmt"
"github.com/gogo/status"
"github.com/prometheus/prometheus/model/labels"
"go.opentelemetry.io/collector/pdata/plog"
Expand Down Expand Up @@ -59,20 +61,28 @@ const (

labelServiceName = "service_name"
serviceUnknown = "unknown_service"
labelLevel = "level"
levelLabel = "detected_level"
logLevelDebug = "debug"
logLevelInfo = "info"
logLevelWarn = "warn"
logLevelError = "error"
logLevelFatal = "fatal"
logLevelCritical = "critical"
logLevelTrace = "trace"
logLevelUnknown = "unknown"
)

var (
maxLabelCacheSize = 100000
rfStats = analytics.NewInt("distributor_replication_factor")
)

var allowedLabelsForLevel = map[string]struct{}{
"level": {}, "LEVEL": {}, "Level": {},
"severity": {}, "SEVERITY": {}, "Severity": {},
"lvl": {}, "LVL": {}, "Lvl": {},
}

// Config for a Distributor.
type Config struct {
// Distributors ring
Expand Down Expand Up @@ -376,7 +386,9 @@ func (d *Distributor) Push(ctx context.Context, req *logproto.PushRequest) (*log
n := 0
pushSize := 0
prevTs := stream.Entries[0].Timestamp
addLogLevel := validationContext.allowStructuredMetadata && validationContext.discoverLogLevels && !lbs.Has(labelLevel)

shouldDiscoverLevels := validationContext.allowStructuredMetadata && validationContext.discoverLogLevels
levelFromLabel, hasLevelLabel := hasAnyLevelLabels(lbs)
for _, entry := range stream.Entries {
if err := d.validator.ValidateEntry(ctx, validationContext, lbs, entry); err != nil {
d.writeFailuresManager.Log(tenantID, err)
Expand All @@ -385,12 +397,21 @@ func (d *Distributor) Push(ctx context.Context, req *logproto.PushRequest) (*log
}

structuredMetadata := logproto.FromLabelAdaptersToLabels(entry.StructuredMetadata)
if addLogLevel && !structuredMetadata.Has(labelLevel) {
logLevel := detectLogLevelFromLogEntry(entry, structuredMetadata)
entry.StructuredMetadata = append(entry.StructuredMetadata, logproto.LabelAdapter{
Name: labelLevel,
Value: logLevel,
})
if shouldDiscoverLevels {
var logLevel string
if hasLevelLabel {
logLevel = levelFromLabel
} else if levelFromMetadata, ok := hasAnyLevelLabels(structuredMetadata); ok {
logLevel = levelFromMetadata
} else {
logLevel = detectLogLevelFromLogEntry(entry, structuredMetadata)
}
if logLevel != logLevelUnknown && logLevel != "" {
entry.StructuredMetadata = append(entry.StructuredMetadata, logproto.LabelAdapter{
Name: levelLabel,
Value: logLevel,
})
}
}
stream.Entries[n] = entry

Expand Down Expand Up @@ -537,6 +558,15 @@ func (d *Distributor) Push(ctx context.Context, req *logproto.PushRequest) (*log
}
}

func hasAnyLevelLabels(l labels.Labels) (string, bool) {
for lbl := range allowedLabelsForLevel {
if l.Has(lbl) {
return l.Get(lbl), true
}
}
return "", false
}

// shardStream shards (divides) the given stream into N smaller streams, where
// N is the sharding size for the given stream. shardSteam returns the smaller
// streams and their associated keys for hashing to ingesters.
Expand Down Expand Up @@ -865,7 +895,11 @@ func detectLogLevelFromLogEntry(entry logproto.Entry, structuredMetadata labels.
if err != nil {
return logLevelInfo
}
if otlpSeverityNumber <= int(plog.SeverityNumberDebug4) {
if otlpSeverityNumber == int(plog.SeverityNumberUnspecified) {
return logLevelUnknown
} else if otlpSeverityNumber <= int(plog.SeverityNumberTrace4) {
return logLevelTrace
} else if otlpSeverityNumber <= int(plog.SeverityNumberDebug4) {
return logLevelDebug
} else if otlpSeverityNumber <= int(plog.SeverityNumberInfo4) {
return logLevelInfo
Expand All @@ -876,74 +910,87 @@ func detectLogLevelFromLogEntry(entry logproto.Entry, structuredMetadata labels.
} else if otlpSeverityNumber <= int(plog.SeverityNumberFatal4) {
return logLevelFatal
}
return logLevelInfo
return logLevelUnknown
}

return extractLogLevelFromLogLine(entry.Line)
}

func extractLogLevelFromLogLine(log string) string {
// check for log levels in known log formats to avoid any false detection
var v string
if isJSON(log) {
v = getValueUsingJSONParser(log)
} else {
v = getValueUsingLogfmtParser(log)
}

// json logs:
switch strings.ToLower(v) {
case "trace", "trc":
return logLevelTrace
case "debug", "dbg":
return logLevelDebug
case "info", "inf":
return logLevelInfo
case "warn", "wrn":
return logLevelWarn
case "error", "err":
return logLevelError
case "critical":
return logLevelCritical
case "fatal":
return logLevelFatal
default:
return detectLevelFromLogLine(log)
}
}

func getValueUsingLogfmtParser(line string) string {
cyriltovena marked this conversation as resolved.
Show resolved Hide resolved
equalIndex := strings.Index(line, "=")
if len(line) == 0 || equalIndex == -1 {
shantanualsi marked this conversation as resolved.
Show resolved Hide resolved
return logLevelUnknown
}
d := logfmt.NewDecoder(strings.NewReader(line))
d.ScanRecord()
for d.ScanKeyval() {
if _, ok := allowedLabelsForLevel[string(d.Key())]; ok {
return string(d.Value())
}
}
return logLevelUnknown
}

func getValueUsingJSONParser(log string) string {
for allowedLabel := range allowedLabelsForLevel {
l, err := jsonparser.GetString([]byte(log), allowedLabel)
if err == nil {
return l
}
}
return logLevelUnknown
}

func isJSON(line string) bool {
var firstNonSpaceChar rune
for _, char := range log {
for _, char := range line {
if !unicode.IsSpace(char) {
firstNonSpaceChar = char
break
}
}

var lastNonSpaceChar rune
for i := len(log) - 1; i >= 0; i-- {
char := rune(log[i])
for i := len(line) - 1; i >= 0; i-- {
char := rune(line[i])
if !unicode.IsSpace(char) {
lastNonSpaceChar = char
break
}
}

if firstNonSpaceChar == '{' && lastNonSpaceChar == '}' {
if strings.Contains(log, `:"err"`) || strings.Contains(log, `:"ERR"`) ||
strings.Contains(log, `:"error"`) || strings.Contains(log, `:"ERROR"`) {
return logLevelError
}
if strings.Contains(log, `:"warn"`) || strings.Contains(log, `:"WARN"`) ||
strings.Contains(log, `:"warning"`) || strings.Contains(log, `:"WARNING"`) {
return logLevelWarn
}
if strings.Contains(log, `:"critical"`) || strings.Contains(log, `:"CRITICAL"`) {
return logLevelCritical
}
if strings.Contains(log, `:"debug"`) || strings.Contains(log, `:"DEBUG"`) {
return logLevelDebug
}
if strings.Contains(log, `:"info"`) || strings.Contains(log, `:"INFO"`) {
return logLevelInfo
}
}

// logfmt logs:
if strings.Contains(log, "=") {
if strings.Contains(log, "=err") || strings.Contains(log, "=ERR") ||
strings.Contains(log, "=error") || strings.Contains(log, "=ERROR") {
return logLevelError
}
if strings.Contains(log, "=warn") || strings.Contains(log, "=WARN") ||
strings.Contains(log, "=warning") || strings.Contains(log, "=WARNING") {
return logLevelWarn
}
if strings.Contains(log, "=critical") || strings.Contains(log, "=CRITICAL") {
return logLevelCritical
}
if strings.Contains(log, "=debug") || strings.Contains(log, "=DEBUG") {
return logLevelDebug
}
if strings.Contains(log, "=info") || strings.Contains(log, "=INFO") {
return logLevelInfo
}
}
return firstNonSpaceChar == '{' && lastNonSpaceChar == '}'
}

func detectLevelFromLogLine(log string) string {
if strings.Contains(log, "err:") || strings.Contains(log, "ERR:") ||
strings.Contains(log, "error") || strings.Contains(log, "ERROR") {
return logLevelError
Expand All @@ -958,7 +1005,5 @@ func extractLogLevelFromLogLine(log string) string {
if strings.Contains(log, "debug:") || strings.Contains(log, "DEBUG:") {
return logLevelDebug
}

// Default to info if no specific level is found
return logLevelInfo
return logLevelUnknown
}
Loading
Loading