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

[pkg/ottl] Add ParseSeverity function #37280

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
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
27 changes: 27 additions & 0 deletions .chloggen/parse-severity-function.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: enhancement

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

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add `ParseSeverity` function to define mappings for log severity levels

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

# (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: []
18 changes: 18 additions & 0 deletions pkg/ottl/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,24 @@ func Test_e2e_converters(t *testing.T) {
m.PutInt("bar", 5)
},
},
{
statement: `set(
attributes["test"],
ParseSeverity(severity_number,
{
"error":[
"err",
{ "min": 3, "max": 4 }
],
"info":[
{ "min": 1, "max": 2 }
],
}
))`,
want: func(tCtx ottllog.TransformContext) {
tCtx.GetLogRecord().Attributes().PutStr("test", "info")
},
},
}

for _, tt := range tests {
Expand Down
9 changes: 4 additions & 5 deletions pkg/ottl/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,10 @@ func (m *mapGetter[K]) Get(ctx context.Context, tCtx K) (any, error) {
evaluated[k] = t
}
}
result := pcommon.NewMap()
if err := result.FromRaw(evaluated); err != nil {
return nil, err
}
return result, nil
// return the raw map instead of creating a pcommon.Map,
// otherwise map structures cannot be used within slices, as the Slice.FromRaw() method
// only supports raw types for its items
return evaluated, nil
}

// TypeError represents that a value was not an expected type.
Expand Down
12 changes: 11 additions & 1 deletion pkg/ottl/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,16 @@ func Test_newGetter(t *testing.T) {
Int: ottltest.Intp(1),
},
},
{
Map: &mapValue{
Values: []mapItem{
{
Key: ottltest.Strp("stringAttr"),
Value: &value{String: ottltest.Strp("value")},
},
},
},
},
},
},
},
Expand All @@ -660,7 +670,7 @@ func Test_newGetter(t *testing.T) {
"foo": map[string]any{
"test": "value",
},
"listAttr": []any{"test0", int64(1)},
"listAttr": []any{"test0", int64(1), map[string]any{"stringAttr": "value"}},
},
"stringAttr": "value",
"intAttr": int64(3),
Expand Down
1 change: 1 addition & 0 deletions pkg/ottl/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/antchfx/xmlquery v1.4.3
github.com/antchfx/xpath v1.3.3
github.com/elastic/go-grok v0.3.1
github.com/go-viper/mapstructure/v2 v2.2.1
github.com/gobwas/glob v0.2.3
github.com/goccy/go-json v0.10.4
github.com/google/uuid v1.6.0
Expand Down
2 changes: 2 additions & 0 deletions pkg/ottl/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions pkg/ottl/ottlfuncs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ Available Converters:
- [ParseCSV](#parsecsv)
- [ParseJSON](#parsejson)
- [ParseKeyValue](#parsekeyvalue)
- [ParseSeverity](#parseseverity)
- [ParseSimplifiedXML](#parsesimplifiedxml)
- [ParseXML](#parsexml)
- [RemoveXML](#removexml)
Expand Down Expand Up @@ -1422,6 +1423,25 @@ Examples:
- `ParseKeyValue("k1!v1_k2!v2_k3!v3", "!", "_")`
- `ParseKeyValue(attributes["pairs"])`

### ParseSeverity

`ParseSeverity(target, severityMapping)`

The `ParseSeverity` converter returns a `string` that represents one of the log levels defined by `severityMapping`.

`target` is a Getter that returns a string or an integer.
`severityMapping` is a map containing the log levels, and a list of values they are mapped from. These values can be either
strings, or map items containing a numeric range, defined by a `min` and `max` key, for the given log level.
For example, the following mapping will map to the `info` level, if the `target` is either a string with the value `inf`,
or an integer in the range `[200,299]`:

`{"info":["inf", {"min":200, "max":299}]}`

Examples:

- `ParseSeverity(attributes["log-level"] {"info":["inf", {"min":200, "max":299}]})`
- `ParseSeverity(severity_number {"info":["inf", {"min":200, "max":299}], "error":[{"min":400, "max":499}]})`

### ParseSimplifiedXML

`ParseSimplifiedXML(target)`
Expand Down
133 changes: 133 additions & 0 deletions pkg/ottl/ottlfuncs/func_parse_severity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"

import (
"context"
"fmt"

"github.com/go-viper/mapstructure/v2"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
)

type ParseSeverityArguments[K any] struct {
Target ottl.Getter[K]
Mapping ottl.PMapGetter[K]
}

func NewParseSeverityFactory[K any]() ottl.Factory[K] {
return ottl.NewFactory("ParseSeverity", &ParseSeverityArguments[K]{}, createParseSeverityFunction[K])
}

func createParseSeverityFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) {
args, ok := oArgs.(*ParseSeverityArguments[K])

if !ok {
return nil, fmt.Errorf("ParseSeverityFactory args must be of type *ParseSeverityArguments[K")
}

return parseSeverity[K](args.Target, args.Mapping), nil
}

func parseSeverity[K any](target ottl.Getter[K], mapping ottl.PMapGetter[K]) ottl.ExprFunc[K] {
return func(ctx context.Context, tCtx K) (any, error) {
severityMap, err := mapping.Get(ctx, tCtx)
if err != nil {
return nil, fmt.Errorf("cannot get severity mapping: %w", err)
}

sev, err := validateSeverity(severityMap.AsRaw())
if err != nil {
return nil, fmt.Errorf("invalid severity mapping: %w", err)
}

value, err := target.Get(ctx, tCtx)
if err != nil {
return nil, fmt.Errorf("could not get log level: %w", err)
}

logLevel, err := evaluateSeverity(value, sev)
if err != nil {
return nil, fmt.Errorf("could not map log level: %w", err)
}

return logLevel, nil
}
}

func validateSeverity(raw map[string]any) (map[string][]any, error) {
s := map[string][]any{}
if err := mapstructure.Decode(raw, &s); err != nil {
return nil, fmt.Errorf("cannot decode severity mapping: %w", err)
}

return s, nil
}

func evaluateSeverity(value any, severities map[string][]any) (string, error) {
for level, criteria := range severities {
match, err := evaluateSeverityMapping(value, criteria)
if err != nil {
return "", fmt.Errorf("could not evaluate log level of value '%v': %w", value, err)
}
if match {
return level, nil
}
}
return "", fmt.Errorf("no matching log level found for value '%v'", value)
}

func evaluateSeverityMapping(value any, criteria []any) (bool, error) {
switch v := value.(type) {
case string:
return evaluateSeverityStringMapping(v, criteria), nil
case int64:
return evaluateSeverityNumberMapping(v, criteria), nil
default:
return false, fmt.Errorf("log level must be either string or int64, but got %T", v)
}
}

func evaluateSeverityNumberMapping(value int64, criteria []any) bool {
for _, crit := range criteria {
// if we have a numeric severity number, we need to match with number ranges
rangeMap, ok := crit.(map[string]any)
if !ok {
continue
}
rangeMin, gotMin := rangeMap["min"]
rangeMax, gotMax := rangeMap["max"]
if !gotMin || !gotMax {
continue
}
rangeMinInt, ok := rangeMin.(int64)
if !ok {
continue
}
rangeMaxInt, ok := rangeMax.(int64)
if !ok {
continue
}
// TODO should we error if the range object does not contain the expected keys/types, or just proceed with checking the other criteria?
if rangeMinInt <= value && rangeMaxInt >= value {
return true
}
}
return false
}

func evaluateSeverityStringMapping(value string, criteria []any) bool {
for _, crit := range criteria {
// if we have a severity string, we need to match with string mappings
criteriaString, ok := crit.(string)
if !ok {
continue
}
if criteriaString == value {
return true
}
}
return false
}
Loading
Loading