diff --git a/.chloggen/14313-loki-convert.yaml b/.chloggen/14313-loki-convert.yaml new file mode 100644 index 0000000000000..a6f40f772e838 --- /dev/null +++ b/.chloggen/14313-loki-convert.yaml @@ -0,0 +1,16 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: exporter/loki + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Automatic mapping beetwen `LogRecord.SeverityNumber` to `LogRecord.Attributes["level"]` + +# One or more tracking issues related to the change +issues: [14313] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: diff --git a/exporter/lokiexporter/README.md b/exporter/lokiexporter/README.md index 82a220ad728ee..25cb556884761 100644 --- a/exporter/lokiexporter/README.md +++ b/exporter/lokiexporter/README.md @@ -113,6 +113,10 @@ by tenant and send requests with the `X-Scope-OrgID` header set to relevant tena If the `loki.tenant` hint attribute is present in both resource or log attributes, then the look-up for a tenant value from resource attributes takes precedence. +## Severity + +OpenTelemetry uses `record.severity` to track log levels where loki uses `record.attributes.level` for the same. The exporter automatically maps the two, except if a "level" attribute already exists. + ## Advanced Configuration Several helper files are leveraged to provide additional capabilities automatically: diff --git a/pkg/translator/loki/logs_to_loki.go b/pkg/translator/loki/logs_to_loki.go index 11a31675eeac6..25a595b31f42a 100644 --- a/pkg/translator/loki/logs_to_loki.go +++ b/pkg/translator/loki/logs_to_loki.go @@ -16,6 +16,7 @@ package loki // import "github.com/open-telemetry/opentelemetry-collector-contri import ( "fmt" + "strings" "github.com/grafana/loki/pkg/logproto" "go.opentelemetry.io/collector/pdata/pcommon" @@ -34,6 +35,10 @@ type PushReport struct { NumDropped int } +const ( + levelAttributeName = "level" +) + // LogsToLokiRequests converts a Logs pipeline data into Loki PushRequests grouped // by tenant. The tenant value is inferred from the `loki.tenant` resource or log // attribute hint. If the `loki.tenant` attribute is present in both resource or @@ -70,6 +75,9 @@ func LogsToLokiRequests(ld plog.Logs) map[string]PushRequest { resource := pcommon.NewResource() rls.At(i).Resource().CopyTo(resource) + // adds level attribute from log.severityNumber + addLogLevelAttributeAndHint(log) + // resolve tenant and get/create a push request group tenant := getTenantFromTenantHint(log.Attributes(), resource.Attributes()) group, ok := groups[tenant] @@ -207,6 +215,9 @@ func LogsToLoki(ld plog.Logs) (*logproto.PushRequest, *PushReport) { resource := pcommon.NewResource() rls.At(i).Resource().CopyTo(resource) + // adds level attribute from log.severityNumber + addLogLevelAttributeAndHint(log) + format := getFormatFromFormatHint(log.Attributes(), resource.Attributes()) mergedLabels := convertAttributesAndMerge(log.Attributes(), resource.Attributes()) @@ -252,3 +263,50 @@ func LogsToLoki(ld plog.Logs) (*logproto.PushRequest, *PushReport) { return pr, report } + +func addLogLevelAttributeAndHint(log plog.LogRecord) { + if log.SeverityNumber() == plog.SeverityNumberUnspecified { + return + } + addHint(log) + if _, found := log.Attributes().Get(levelAttributeName); !found { + level := severityNumberToLevel[log.SeverityNumber().String()] + log.Attributes().PutStr(levelAttributeName, level) + } +} + +func addHint(log plog.LogRecord) { + if value, found := log.Attributes().Get(hintAttributes); found && !strings.Contains(value.AsString(), levelAttributeName) { + log.Attributes().PutStr(hintAttributes, fmt.Sprintf("%s,%s", value.AsString(), levelAttributeName)) + } else { + log.Attributes().PutStr(hintAttributes, levelAttributeName) + } +} + +var severityNumberToLevel = map[string]string{ + plog.SeverityNumberUnspecified.String(): "UNSPECIFIED", + plog.SeverityNumberTrace.String(): "TRACE", + plog.SeverityNumberTrace2.String(): "TRACE2", + plog.SeverityNumberTrace3.String(): "TRACE3", + plog.SeverityNumberTrace4.String(): "TRACE4", + plog.SeverityNumberDebug.String(): "DEBUG", + plog.SeverityNumberDebug2.String(): "DEBUG2", + plog.SeverityNumberDebug3.String(): "DEBUG3", + plog.SeverityNumberDebug4.String(): "DEBUG4", + plog.SeverityNumberInfo.String(): "INFO", + plog.SeverityNumberInfo2.String(): "INFO2", + plog.SeverityNumberInfo3.String(): "INFO3", + plog.SeverityNumberInfo4.String(): "INFO4", + plog.SeverityNumberWarn.String(): "WARN", + plog.SeverityNumberWarn2.String(): "WARN2", + plog.SeverityNumberWarn3.String(): "WARN3", + plog.SeverityNumberWarn4.String(): "WARN4", + plog.SeverityNumberError.String(): "ERROR", + plog.SeverityNumberError2.String(): "ERROR2", + plog.SeverityNumberError3.String(): "ERROR3", + plog.SeverityNumberError4.String(): "ERROR4", + plog.SeverityNumberFatal.String(): "FATAL", + plog.SeverityNumberFatal2.String(): "FATAL2", + plog.SeverityNumberFatal3.String(): "FATAL3", + plog.SeverityNumberFatal4.String(): "FATAL4", +} diff --git a/pkg/translator/loki/logs_to_loki_test.go b/pkg/translator/loki/logs_to_loki_test.go index c01a4f2cc44bf..f1d247128e9aa 100644 --- a/pkg/translator/loki/logs_to_loki_test.go +++ b/pkg/translator/loki/logs_to_loki_test.go @@ -244,12 +244,14 @@ func TestLogsToLokiRequestWithGroupingByTenant(t *testing.T) { func TestLogsToLokiRequestWithoutTenant(t *testing.T) { testCases := []struct { - desc string - hints map[string]interface{} - attrs map[string]interface{} - res map[string]interface{} - expectedLabel string - expectedLines []string + desc string + hints map[string]interface{} + attrs map[string]interface{} + res map[string]interface{} + severity plog.SeverityNumber + levelAttribute string + expectedLabel string + expectedLines []string }{ { desc: "with attribute to label and regular attribute", @@ -300,6 +302,27 @@ func TestLogsToLokiRequestWithoutTenant(t *testing.T) { `traceID=03000000000000000000000000000000 attribute_http.status=200`, }, }, + { + desc: "with severity to label", + severity: plog.SeverityNumberDebug4, + expectedLabel: `{exporter="OTLP", level="DEBUG4"}`, + expectedLines: []string{ + `{"traceid":"01000000000000000000000000000000"}`, + `{"traceid":"02000000000000000000000000000000"}`, + `{"traceid":"03000000000000000000000000000000"}`, + }, + }, + { + desc: "with severity, already existing level", + severity: plog.SeverityNumberDebug4, + levelAttribute: "dummy", + expectedLabel: `{exporter="OTLP", level="dummy"}`, + expectedLines: []string{ + `{"traceid":"01000000000000000000000000000000"}`, + `{"traceid":"02000000000000000000000000000000"}`, + `{"traceid":"03000000000000000000000000000000"}`, + }, + }, } for _, tt := range testCases { t.Run(tt.desc, func(t *testing.T) { @@ -310,6 +333,10 @@ func TestLogsToLokiRequestWithoutTenant(t *testing.T) { ld.ResourceLogs().At(0).ScopeLogs().AppendEmpty() ld.ResourceLogs().At(0).ScopeLogs().At(i).LogRecords().AppendEmpty() ld.ResourceLogs().At(0).ScopeLogs().At(i).LogRecords().At(0).SetTraceID([16]byte{byte(i + 1)}) + ld.ResourceLogs().At(0).ScopeLogs().At(i).LogRecords().At(0).SetSeverityNumber(tt.severity) + if len(tt.levelAttribute) > 0 { + ld.ResourceLogs().At(0).ScopeLogs().At(i).LogRecords().At(0).Attributes().PutStr(levelAttributeName, tt.levelAttribute) + } } if len(tt.res) > 0 { @@ -355,12 +382,14 @@ func TestLogsToLokiRequestWithoutTenant(t *testing.T) { func TestLogsToLoki(t *testing.T) { testCases := []struct { - desc string - hints map[string]interface{} - attrs map[string]interface{} - res map[string]interface{} - expectedLabel string - expectedLines []string + desc string + hints map[string]interface{} + attrs map[string]interface{} + res map[string]interface{} + severity plog.SeverityNumber + levelAttribute string + expectedLabel string + expectedLines []string }{ { desc: "with attribute to label and regular attribute", @@ -411,6 +440,27 @@ func TestLogsToLoki(t *testing.T) { `traceID=01020304050600000000000000000000 resource_region.az=eu-west-1a`, }, }, + { + desc: "with severity to label", + severity: plog.SeverityNumberDebug4, + expectedLabel: `{exporter="OTLP", level="DEBUG4"}`, + expectedLines: []string{ + `{"traceid":"01020304000000000000000000000000"}`, + `{"traceid":"01020304050000000000000000000000"}`, + `{"traceid":"01020304050600000000000000000000"}`, + }, + }, + { + desc: "with severity, already existing level", + severity: plog.SeverityNumberDebug4, + levelAttribute: "dummy", + expectedLabel: `{exporter="OTLP", level="dummy"}`, + expectedLines: []string{ + `{"traceid":"01020304000000000000000000000000"}`, + `{"traceid":"01020304050000000000000000000000"}`, + `{"traceid":"01020304050600000000000000000000"}`, + }, + }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { @@ -421,8 +471,11 @@ func TestLogsToLoki(t *testing.T) { ld.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().AppendEmpty() ld.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().AppendEmpty() ld.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().AppendEmpty() + ld.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).SetSeverityNumber(tC.severity) ld.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).SetTraceID(pcommon.TraceID([16]byte{1, 2, 3, 4})) + ld.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(1).SetSeverityNumber(tC.severity) ld.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(1).SetTraceID(pcommon.TraceID([16]byte{1, 2, 3, 4, 5})) + ld.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(2).SetSeverityNumber(tC.severity) ld.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(2).SetTraceID(pcommon.TraceID([16]byte{1, 2, 3, 4, 5, 6})) // copy the attributes from the test case to the log entry @@ -434,6 +487,11 @@ func TestLogsToLoki(t *testing.T) { if len(tC.res) > 0 { ld.ResourceLogs().At(0).Resource().Attributes().FromRaw(tC.res) } + if len(tC.levelAttribute) > 0 { + ld.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).Attributes().PutStr(levelAttributeName, tC.levelAttribute) + ld.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(1).Attributes().PutStr(levelAttributeName, tC.levelAttribute) + ld.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(2).Attributes().PutStr(levelAttributeName, tC.levelAttribute) + } // we can't use copy here, as the value (Value) will be used as string lookup later, so, we need to convert it to string now for k, v := range tC.hints {