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 complex indexing to paths and converters #20754

Merged
Merged
Show file tree
Hide file tree
Changes from 14 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
16 changes: 16 additions & 0 deletions .chloggen/ottl-support-indexing-3.yaml
Original file line number Diff line number Diff line change
@@ -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: pkg/ottl

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add support for complex indexing of Paths and Converters to allow accessing nested items.

# One or more tracking issues related to the change
issues: [20754]

# (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: All components using OTTL can take advantage of this feature.
32 changes: 28 additions & 4 deletions pkg/ottl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,35 @@ Values are passed as input to an Invocation or are used in a Boolean Expression.
- [Literals](#literals)
- [Enums](#enums)
- [Converters](#converters)
- [Math Expressions](#math_expressions)
- [Math Expressions](#math-expressions)

#### Paths

A Path Value is a reference to a telemetry field. Paths are made up of lowercase identifiers, dots (`.`), and square brackets combined with a string key (`["key"]`). **The interpretation of a Path is NOT implemented by the OTTL.** Instead, the user must provide a `PathExpressionParser` that the OTTL can use to interpret paths. As a result, how the Path parts are used is up to the user. However, it is recommended, that the parts be used like so:
A Path Value is a reference to a telemetry field. Paths are made up of lowercase identifiers, dots (`.`), and square brackets combined with a string key (`["key"]`) or int key (`[0]`). **The interpretation of a Path is NOT implemented by the OTTL.** Instead, the user must provide a `PathExpressionParser` that the OTTL can use to interpret paths. As a result, how the Path parts are used is up to the user. However, it is recommended that the parts be used like so:

- Identifiers are used to map to a telemetry field.
- Dots (`.`) are used to separate nested fields.
- Square brackets and keys (`["key"]`) are used to access values within maps.

When accessing a map's value, if the given key does not exist, `nil` will be returned.
This can be used to check for the presence of a key within a map within a [Boolean Expression](#boolean_expressions).
This can be used to check for the presence of a key within a map within a [Boolean Expression](#boolean-expressions).

Example Paths
- `name`
- `value_double`
- `resource.name`
- `resource.attributes["key"]`
- `attributes["nested"]["values"]`
- `cache["slice"][1]`

##### Contexts

The package that handles the interpretation of a path is normally called a Context.
Contexts will have an implementation of `PathExpressionParser` that decides how an OTTL Path is interpreted.
The context's implementation will need to make decisions like what a dot (`.`) represents or which paths allow indexing (`["key"]`) and how many indexes.

[There are OpenTelemetry-specific contexts provided for each signal here.](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/ottl/contexts)
When using OTTL it is recommended to use these contexts unless you have a specific need. Check out each context to view the paths it supports.

#### Lists

Expand Down Expand Up @@ -116,19 +127,32 @@ When defining a function that will be used as an Invocation by the OTTL, if the
#### Converters

Converters are special functions that convert data to a new format before being passed to an Invocation or Boolean Expression.
Like Invocations, Converters are made up of 2 parts:
Converters are made up of 3 parts:

- a string identifier. The string identifier must start with an uppercase letter.
- zero or more Values (comma separated) surrounded by parentheses (`()`).
- a combination of zero or more a string key (`["key"]`) or int key (`[0]`)

**The OTTL does not define any converter implementations.**
Users must include converters in the same map that invocations are supplied.
The OTTL will use this map and reflection to generate Converters that can then be invoked by the user.
See [ottlfuncs](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/ottl/ottlfuncs#converters) for pre-made, usable Converters.

When keys are supplied the value returned by the Converter will be indexed by the keys in order.
If keys are supplied to a Converter and the return value cannot be indexed, or if the return value doesn't support the
type of key supplied, OTTL will error. Supported values are:

| Type | Index Type |
|------------------|------------|
| `pcommon.Map` | `String` |
| `map[string]any` | `String` |
| `pcommon.Slice` | `Int` |
| `[]any` | `Int` |

Example Converters
- `Int()`
- `IsMatch(field, ".*")`
- `Split(field, ",")[1]`


#### Math Expressions
Expand Down
124 changes: 124 additions & 0 deletions pkg/ottl/contexts/internal/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

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

import (
"fmt"

"go.opentelemetry.io/collector/pdata/pcommon"

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

func GetMapValue(m pcommon.Map, keys []ottl.Key) (interface{}, error) {
evan-bradley marked this conversation as resolved.
Show resolved Hide resolved
if len(keys) == 0 {
return nil, fmt.Errorf("cannot get map value without key")
}
if keys[0].String == nil {
return nil, fmt.Errorf("non-string indexing is not supported")
}

val, ok := m.Get(*keys[0].String)
if !ok {
return nil, nil
}
for i := 1; i < len(keys); i++ {
switch val.Type() {
case pcommon.ValueTypeMap:
if keys[i].String == nil {
return nil, fmt.Errorf("map must be indexed by a string")
}
val, ok = val.Map().Get(*keys[i].String)
if !ok {
return nil, nil
}
case pcommon.ValueTypeSlice:
if keys[i].Int == nil {
return nil, fmt.Errorf("slice must be indexed by an int")
}
if int(*keys[i].Int) >= val.Slice().Len() || int(*keys[i].Int) < 0 {
return nil, fmt.Errorf("index %v out of bounds", *keys[i].Int)
}
val = val.Slice().At(int(*keys[i].Int))
default:
return nil, fmt.Errorf("type %v does not support string indexing", val.Type())
}
}
return ottlcommon.GetValue(val), nil
}

func SetMapValue(m pcommon.Map, keys []ottl.Key, val interface{}) error {
if len(keys) == 0 {
return fmt.Errorf("cannot set map value without key")
}
if keys[0].String == nil {
return fmt.Errorf("non-string indexing is not supported")
}

TylerHelmuth marked this conversation as resolved.
Show resolved Hide resolved
var newValue pcommon.Value
switch val.(type) {
case []string, []bool, []int64, []float64, [][]byte, []any:
newValue = pcommon.NewValueSlice()
default:
newValue = pcommon.NewValueEmpty()
}
err := SetValue(newValue, val)
if err != nil {
return err
}

currentValue, ok := m.Get(*keys[0].String)
if !ok {
currentValue = m.PutEmpty(*keys[0].String)
}

for i := 1; i < len(keys); i++ {
switch currentValue.Type() {
case pcommon.ValueTypeMap:
if keys[i].String == nil {
return fmt.Errorf("map must be indexed by a string")
}
potentialValue, ok := currentValue.Map().Get(*keys[i].String)
if !ok {
currentValue = currentValue.Map().PutEmpty(*keys[i].String)
evan-bradley marked this conversation as resolved.
Show resolved Hide resolved
} else {
currentValue = potentialValue
}
case pcommon.ValueTypeSlice:
if keys[i].Int == nil {
return fmt.Errorf("slice must be indexed by an int")
}
if int(*keys[i].Int) >= currentValue.Slice().Len() || int(*keys[i].Int) < 0 {
return fmt.Errorf("index %v out of bounds", *keys[i].Int)
}
currentValue = currentValue.Slice().At(int(*keys[i].Int))
case pcommon.ValueTypeEmpty:
if keys[i].String != nil {
currentValue = currentValue.SetEmptyMap().PutEmpty(*keys[i].String)
} else if keys[i].Int != nil {
currentValue.SetEmptySlice()
for k := 0; k < int(*keys[i].Int); k++ {
currentValue.Slice().AppendEmpty()
}
currentValue = currentValue.Slice().AppendEmpty()
}
default:
return fmt.Errorf("type %v does not support string indexing", currentValue.Type())
}
}
newValue.CopyTo(currentValue)
return nil
}
Loading