Skip to content

Commit

Permalink
[translator/loki] Fixes a panic that occurred during the promotion of…
Browse files Browse the repository at this point in the history
… nested attributes containing dots to labels (#25142)

When the value for loki hint attribute contains dots (more than 1 dot)
and attributes are nested so some level contains dotted attribute name
(for example `{"log": {"file.name": "foo"}}`), the getNestedAttribute
function throws a panic

Co-authored-by: Tyler Helmuth <[email protected]>
  • Loading branch information
mar4uk and TylerHelmuth authored Aug 14, 2023
1 parent bba1b43 commit 69e7b5a
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 30 deletions.
27 changes: 27 additions & 0 deletions .chloggen/fix-loki-translator-nested-attributes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: bug_fix

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: lokitranslator, lokiexporter

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Fixes a panic that occurred during the promotion of nested attributes containing dots to labels

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [25125]

# (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:

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: []
37 changes: 20 additions & 17 deletions pkg/translator/loki/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const (
levelLabel string = "level"
)

const attrSeparator = "."

func convertAttributesAndMerge(logAttrs pcommon.Map, resAttrs pcommon.Map, defaultLabelsEnabled map[string]bool) model.LabelSet {
out := getDefaultLabels(resAttrs, defaultLabelsEnabled)

Expand Down Expand Up @@ -97,33 +99,34 @@ func convertAttributesToLabels(attributes pcommon.Map, attrsToSelect pcommon.Val
for _, attr := range attrs {
attr = strings.TrimSpace(attr)

av, ok := attributes.Get(attr)
if !ok {
// couldn't find the attribute under the given name directly
// perhaps it's a nested attribute?
av, ok = getNestedAttribute(attr, attributes) // shadows the OK from above on purpose
}

if ok {
if av, ok := getAttribute(attr, attributes); ok {
out[model.LabelName(attr)] = model.LabelValue(av.AsString())
}
}

return out
}

func getNestedAttribute(attr string, attributes pcommon.Map) (pcommon.Value, bool) {
left, right, _ := strings.Cut(attr, ".")
av, ok := attributes.Get(left)
if !ok {
return pcommon.Value{}, false
}

if len(right) == 0 {
func getAttribute(attr string, attributes pcommon.Map) (pcommon.Value, bool) {
if av, ok := attributes.Get(attr); ok {
return av, ok
}

return getNestedAttribute(right, av.Map())
// couldn't find the attribute under the given name directly
// perhaps it's a nested attribute?
segments := strings.Split(attr, attrSeparator)
segmentsNumber := len(segments)
for i := 0; i < segmentsNumber-1; i++ {
left := strings.Join(segments[:segmentsNumber-i-1], attrSeparator)
right := strings.Join(segments[segmentsNumber-i-1:], attrSeparator)

if av, ok := getAttribute(left, attributes); ok {
if av.Type() == pcommon.ValueTypeMap {
return getAttribute(right, av.Map())
}
}
}
return pcommon.Value{}, false
}

func parseAttributeNames(attrsToSelect pcommon.Value) []string {
Expand Down
103 changes: 90 additions & 13 deletions pkg/translator/loki/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,22 +290,99 @@ func TestRemoveAttributes(t *testing.T) {
}
}

func TestGetNestedAttribute(t *testing.T) {
// prepare
attrs := pcommon.NewMap()
err := attrs.FromRaw(map[string]interface{}{
"host": map[string]interface{}{
"name": "guarana",
func TestGetAttribute(t *testing.T) {
testCases := []struct {
desc string
attr string
attrs map[string]interface{}
expected pcommon.Value
ok bool
}{
{
desc: "attributes don't contain dotted names",
attr: "host.name",
attrs: map[string]interface{}{
"host": map[string]interface{}{
"name": "guarana",
},
},
expected: pcommon.NewValueStr("guarana"),
ok: true,
},
})
require.NoError(t, err)
{
desc: "attributes contain dotted name on the nested level",
attr: "log.file.name",
attrs: map[string]interface{}{
"log": map[string]interface{}{
"file.name": "foo",
},
},
expected: pcommon.NewValueStr("foo"),
ok: true,
},
{
desc: "attributes contain dotted name on the upper level",
attr: "log.file.name",
attrs: map[string]interface{}{
"log.file": map[string]interface{}{
"name": "foo",
},
},
expected: pcommon.NewValueStr("foo"),
ok: true,
},
{
desc: "attributes contain dotted attribute",
attr: "log.file.name",
attrs: map[string]interface{}{
"log.file.name": "foo",
},
expected: pcommon.NewValueStr("foo"),
ok: true,
},
{
desc: "dotted name that doesn't match attr",
attr: "log.file.name",
attrs: map[string]interface{}{
"log.file": "foo",
},
expected: pcommon.Value{},
ok: false,
},
{
desc: "should get the longest match",
attr: "log.file.name",
attrs: map[string]interface{}{
"log.file.name": "foo",
"log": map[string]interface{}{
"file": map[string]interface{}{
"name": "bar",
},
},
"log.file": map[string]interface{}{
"name": "baz",
},
},
expected: pcommon.NewValueStr("foo"),
ok: true,
},
}

for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
// prepare
attrs := pcommon.NewMap()
err := attrs.FromRaw(tC.attrs)
require.NoError(t, err)

// test
attr, ok := getNestedAttribute("host.name", attrs)
// test
attr, ok := getAttribute(tC.attr, attrs)

// verify
assert.Equal(t, "guarana", attr.AsString())
assert.True(t, ok)
// verify
assert.Equal(t, tC.expected, attr)
assert.Equal(t, tC.ok, ok)
})
}
}

func TestConvertLogToLogRawEntry(t *testing.T) {
Expand Down

0 comments on commit 69e7b5a

Please sign in to comment.