Skip to content

Commit

Permalink
[pkg/ottl] add duration converter function (open-telemetry#23659)
Browse files Browse the repository at this point in the history
Description: Added a converter function that takes a string
representation of a duration and converts it to a Golang duration.

Link to tracking Issue:
open-telemetry#22015

Testing: Unit tests for stringGetter to duration.

Documentation:

---------

Co-authored-by: Tyler Helmuth <12352919+TylerHelmuth@users.noreply.github.com>
  • Loading branch information
fchikwekwe and TylerHelmuth authored Jul 10, 2023
1 parent 0e4cb4a commit 456849a
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .chloggen/feat_duration-func.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Use this changelog template to create an entry for release notes.
# If your change doesn't affect end users, such as a test fix or a tooling change,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.

# 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 new `Duration` converter to convert string to a Golang time.duration"

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

# (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:
30 changes: 30 additions & 0 deletions pkg/ottl/ottlfuncs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ Available Converters:
- [Concat](#concat)
- [ConvertCase](#convertcase)
- [FNV](#fnv)
- [Duration](#duration)
- [Int](#int)
- [IsMap](#ismap)
- [IsMatch](#ismatch)
Expand All @@ -288,6 +289,7 @@ Available Converters:
- [SHA256](#sha256)
- [SpanID](#spanid)
- [Split](#split)
- [Time](#time)
- [TraceID](#traceid)
- [Substring](#substring)
- [UUID](#UUID)
Expand Down Expand Up @@ -335,6 +337,22 @@ Examples:

- `ConvertCase(metric.name, "snake")`

### Duration

`Duration(duration)`

The `Duration` Converter takes a string representation of a duration and converts it to a Golang `time.duration`.

`duration` is a string.

If either `duration` is nil or is in a format that cannot be converted to Golang `time.duration`, an error is returned.

Examples:

- `Duration("3s")`
- `Duration("333ms")`
- `Duration("1000000h")`

### FNV

`FNV(value)`
Expand Down Expand Up @@ -569,6 +587,18 @@ Examples:

- ```Split("A|B|C", "|")```

### Time

The `Time` Converter takes a string representation of a time and converts it to a Golang `time.Time`.

`time` is a string. `format` is a string.

If either `time` or `format` are nil, an error is returned. The parser used is the parser at [internal/coreinternal/parser](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/internal/coreinternal/timeutils). If the time and format do not follow the parsing rules used by this parser, an error is returned.

Examples:

- `Time("02/04/2023", "%m/%d/%Y")`

### TraceID

`TraceID(bytes)`
Expand Down
43 changes: 43 additions & 0 deletions pkg/ottl/ottlfuncs/func_duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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"
"time"

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

type DurationArguments[K any] struct {
Duration ottl.StringGetter[K] `ottlarg:"0"`
}

func NewDurationFactory[K any]() ottl.Factory[K] {
return ottl.NewFactory("Duration", &DurationArguments[K]{}, createDurationFunction[K])
}
func createDurationFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) {
args, ok := oArgs.(*DurationArguments[K])

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

return Duration(args.Duration)
}

func Duration[K any](duration ottl.StringGetter[K]) (ottl.ExprFunc[K], error) {
return func(ctx context.Context, tCtx K) (interface{}, error) {
d, err := duration.Get(ctx, tCtx)
if err != nil {
return nil, err
}
dur, err := time.ParseDuration(d)
if err != nil {
return nil, err
}
return dur, nil
}, nil
}
192 changes: 192 additions & 0 deletions pkg/ottl/ottlfuncs/func_duration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package ottlfuncs

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

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

func Test_Duration(t *testing.T) {
tests := []struct {
name string
duration ottl.StringGetter[interface{}]
expected time.Duration
}{
{
name: "100 milliseconds",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "100ms", nil
},
},
expected: time.Duration(100000000),
}, {
name: "234 microseconds",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "234us", nil
},
},
expected: time.Duration(234000),
}, {
name: "777 nanoseconds",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "777ns", nil
},
},
expected: time.Duration(777),
},
{
name: "one second",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "1s", nil
},
},
expected: time.Duration(1000000000),
},
{
name: "two hundred second",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "200s", nil
},
},
expected: time.Duration(200000000000),
},
{
name: "three minutes",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "3m", nil
},
},
expected: time.Duration(180000000000),
},
{
name: "45 minutes",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "45m", nil
},
},
expected: time.Duration(2700000000000),
},
{
name: "7 mins, 12 secs",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "7m12s", nil
},
},
expected: time.Duration(432000000000),
},
{
name: "4 hours",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "4h", nil
},
},
expected: time.Duration(14400000000000),
},
{
name: "5 hours, 23 mins, 59 secs",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "5h23m59s", nil
},
},
expected: time.Duration(19439000000000),
},
{
name: "5 hours, 59 secs",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "5h59s", nil
},
},
expected: time.Duration(18059000000000),
},
{
name: "5 hours, 23 mins",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "5h23m", nil
},
},
expected: time.Duration(19380000000000),
},
{
name: "2 mins, 1 sec, 64 microsecs",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "2m1s64us", nil
},
},
expected: time.Duration(121000064000),
},
{
name: "59 hours, 1 min, 78 millisecs",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "59h1m78ms", nil
},
},
expected: time.Duration(212460078000000),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
exprFunc, err := Duration(tt.duration)
assert.NoError(t, err)
result, err := exprFunc(nil, nil)
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}

func Test_DurationError(t *testing.T) {
tests := []struct {
name string
duration ottl.StringGetter[interface{}]
expectedError string
}{
{
name: "empty duration",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "", nil
},
},
expectedError: "invalid duration",
},
{
name: "empty duration",
duration: &ottl.StandardStringGetter[interface{}]{
Getter: func(ctx context.Context, tCtx interface{}) (interface{}, error) {
return "one second", nil
},
},
expectedError: "invalid duration",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
exprFunc, err := Duration[any](tt.duration)
require.NoError(t, err)
_, err = exprFunc(context.Background(), nil)
assert.ErrorContains(t, err, tt.expectedError)
})
}
}
1 change: 1 addition & 0 deletions pkg/ottl/ottlfuncs/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func converters[K any]() []ottl.Factory[K] {
// Converters
NewConcatFactory[K](),
NewConvertCaseFactory[K](),
NewDurationFactory[K](),
NewFnvFactory[K](),
NewIntFactory[K](),
NewIsMapFactory[K](),
Expand Down

0 comments on commit 456849a

Please sign in to comment.