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

[filebeat][httpjson]- Added min & max functions to the template engine #36036

Merged
merged 14 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ automatic splitting at root level, if root level element is an array. {pull}3415
- Add device support for Azure AD entity analytics. {pull}35807[35807]
- Improve CEL input performance. {pull}35915[35915]
- Adding filename details from zip to response for httpjson {issue}33952[33952] {pull}34044[34044]
- Added support for min/max template functions in httpjson input. {issue}4414[4414] {pull}36036[36036]
ShourieG marked this conversation as resolved.
Show resolved Hide resolved
- Add `clean_session` configuration setting for MQTT input. {pull}35806[16204]
- Add fingerprint mode for the filestream scanner and new file identity based on it {issue}34419[34419] {pull}35734[35734]

Expand Down
2 changes: 2 additions & 0 deletions x-pack/filebeat/docs/inputs/input-httpjson.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ Some built-in helper functions are provided to work with the input state inside
- `hmacBase64`: calculates the hmac signature of a list of strings concatenated together. Returns a base64 encoded signature. Supports sha1 or sha256. Example `[[hmac "sha256" "secret" "string1" "string2" (formatDate (now) "RFC1123")]]`
- `hmac`: calculates the hmac signature of a list of strings concatenated together. Returns a hex encoded signature. Supports sha1 or sha256. Example `[[hmac "sha256" "secret" "string1" "string2" (formatDate (now) "RFC1123")]]`
- `join`: joins a list using the specified separator. Example: `[[join .body.arr ","]]`
- `max`: returns the maximum value of a list of numbers(int, uint, float) where at least one value is required. If no values are provided, it returns an error.
- `min`: returns the minimum value of a list of numbers(int, uint, float) where at least one value is required. If no values are provided, it returns an error.
- `mul`: multiplies two integers.
- `now`: returns the current `time.Time` object in UTC. Optionally, it can receive a `time.Duration` as a parameter. Example: `[[now (parseDuration "-1h")]]` returns the time at 1 hour before now.
- `parseDate`: parses a date string and returns a `time.Time` in UTC. By default the expected layout is `RFC3339` but optionally can accept any of the Golang predefined layouts or a custom one. Example: `[[ parseDate "2020-11-05T12:25:32Z" ]]`, `[[ parseDate "2020-11-05T12:25:32.1234567Z" "RFC3339Nano" ]]`, `[[ (parseDate "Thu Nov 5 12:25:32 +0000 2020" "Mon Jan _2 15:04:05 -0700 2006").UTC ]]`.
Expand Down
188 changes: 188 additions & 0 deletions x-pack/filebeat/input/httpjson/value_tpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"errors"
"fmt"
"hash"
"math"
"net/url"
"reflect"
"regexp"
Expand All @@ -40,6 +41,11 @@ const (
var (
errEmptyTemplateResult = errors.New("the template result is empty")
errExecutingTemplate = errors.New("the template execution failed")
errMinWrongNoOfArgs = errors.New("template: :1:2: executing \"\" at <min>: wrong number of args for min: want at least 1 got 0")
errMaxWrongNoOfArgs = errors.New("template: :1:2: executing \"\" at <max>: wrong number of args for max: want at least 1 got 0")
errMinUnknownType = errors.New("template: :1:2: executing \"\" at <min 1 \"b\" -2>: error calling min: unknown type for \"b\" (string)")
errMaxUnknownType = errors.New("template: :1:2: executing \"\" at <max 1 \"b\" -2>: error calling max: unknown type for \"b\" (string)")
ShourieG marked this conversation as resolved.
Show resolved Hide resolved
errStrUnknownType = "unknown type for %q (%T)"
ShourieG marked this conversation as resolved.
Show resolved Hide resolved
)

type valueTpl struct {
Expand All @@ -66,6 +72,8 @@ func (t *valueTpl) Unpack(in string) error {
"hmacBase64": hmacStringBase64,
"join": join,
"toJSON": toJSON,
"max": maximum,
"min": minimum,
"mul": mul,
"now": now,
"parseDate": parseDate,
Expand Down Expand Up @@ -295,6 +303,186 @@ func div(a, b int64) int64 {
return a / b
}

// minimum returns the minimum of a and an arbitrary number of values.
func minimum(a interface{}, nums ...interface{}) (interface{}, error) {
var min interface{}
var err error

min = math.MaxInt
nums = append(nums, a)
if len(nums) == 1 {
return nums[0], nil
}
for _, num := range nums {
min, err = findMin(min, num)
if err != nil {
return nil, err
}
}
return min, nil
}

// findMin returns the minimum of two values.
func findMin(b, a interface{}) (interface{}, error) {
ShourieG marked this conversation as resolved.
Show resolved Hide resolved
ai := reflect.ValueOf(a)
bi := reflect.ValueOf(b)

switch ai.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
switch bi.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if ai.Int() < bi.Int() {
return ai.Int(), nil
}
return bi.Int(), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if ai.Int() < int64(bi.Uint()) {
return ai.Int(), nil
}
return bi.Uint(), nil
case reflect.Float32, reflect.Float64:
if float64(ai.Int()) < bi.Float() {
return ai.Int(), nil
}
return bi.Float(), nil
default:
return nil, fmt.Errorf(errStrUnknownType, bi, b)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
switch bi.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if int64(ai.Uint()) < bi.Int() {
return ai.Uint(), nil
}
return bi.Int(), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if ai.Uint() < bi.Uint() {
return ai.Uint(), nil
}
return bi.Uint(), nil
case reflect.Float32, reflect.Float64:
if float64(ai.Uint()) < bi.Float() {
return ai.Uint(), nil
}
return bi.Float(), nil
default:
return nil, fmt.Errorf(errStrUnknownType, bi, b)
}
case reflect.Float32, reflect.Float64:
switch bi.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if ai.Float() < float64(bi.Int()) {
return ai.Float(), nil
}
return bi.Int(), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if ai.Float() < float64(bi.Uint()) {
return ai.Float(), nil
}
return bi.Uint(), nil
case reflect.Float32, reflect.Float64:
if ai.Float() < bi.Float() {
return ai.Float(), nil
}
return bi.Float(), nil
default:
return nil, fmt.Errorf(errStrUnknownType, bi, b)
}
default:
return nil, fmt.Errorf(errStrUnknownType, ai, a)
}
}

// maximum returns the maximum of a and an arbitrary number of values.
func maximum(a interface{}, nums ...interface{}) (interface{}, error) {
var max interface{}
var err error

max = math.MinInt
nums = append(nums, a)
if len(nums) == 1 {
return nums[0], nil
}
for _, num := range nums {
max, err = findMax(max, num)
if err != nil {
return nil, err
}
}
return max, nil
}

// finds the maximum of two values
func findMax(b, a interface{}) (interface{}, error) {
ai := reflect.ValueOf(a)
bi := reflect.ValueOf(b)

switch ai.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
switch bi.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if ai.Int() > bi.Int() {
return ai.Int(), nil
}
return bi.Int(), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if ai.Int() > int64(bi.Uint()) {
return ai.Int(), nil
}
return bi.Uint(), nil
case reflect.Float32, reflect.Float64:
if float64(ai.Int()) > bi.Float() {
return ai.Int(), nil
}
return bi.Float(), nil
default:
return nil, fmt.Errorf(errStrUnknownType, bi, b)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
switch bi.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if int64(ai.Uint()) > bi.Int() {
return ai.Uint(), nil
}
return bi.Int(), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if ai.Uint() > bi.Uint() {
return ai.Uint(), nil
}
return bi.Uint(), nil
case reflect.Float32, reflect.Float64:
if float64(ai.Uint()) > bi.Float() {
return ai.Uint(), nil
}
return bi.Float(), nil
default:
return nil, fmt.Errorf(errStrUnknownType, bi, b)
}
case reflect.Float32, reflect.Float64:
switch bi.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if ai.Float() > float64(bi.Int()) {
return ai.Float(), nil
}
return bi.Int(), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if ai.Float() > float64(bi.Uint()) {
return ai.Float(), nil
}
return bi.Uint(), nil
case reflect.Float32, reflect.Float64:
if ai.Float() > bi.Float() {
return ai.Float(), nil
}
return bi.Float(), nil
default:
return nil, fmt.Errorf(errStrUnknownType, bi, b)
}
default:
return nil, fmt.Errorf(errStrUnknownType, ai, a)
}
}

func base64Encode(values ...string) string {
data := strings.Join(values, "")
if data == "" {
Expand Down
98 changes: 98 additions & 0 deletions x-pack/filebeat/input/httpjson/value_tpl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,104 @@ func TestValueTpl(t *testing.T) {
paramTr: transformable{},
expectedVal: "4",
},
{
name: "func max",
value: `[[max 1 4]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "4",
},
{
name: "func max with list",
value: `[[max 1 4 5 3 2]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "5",
},
{
name: "func max with year expression",
value: `[[max (now.Year) 2023]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "2023",
},
{
name: "func max with single value",
value: `[[max 2023]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "2023",
},
{
name: "func max with no arguments",
value: `[[max]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedError: errMaxWrongNoOfArgs.Error(),
},
{
name: "func max with mixed type argument list",
value: `[[max 4 6.24 2.1 1 -3.3 -1.2 7]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "7",
},
{
name: "func max with unknown type in list",
value: `[[max 1 "b" -2]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedError: errMaxUnknownType.Error(),
},
{
name: "func min",
value: `[[min 1 4]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "1",
},
{
name: "func min with list",
value: `[[min 4 6 2 1 3 7]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "1",
},
ShourieG marked this conversation as resolved.
Show resolved Hide resolved
{
name: "func min with mixed list",
value: `[[min 4 6.24 2.1 1 -3.3 -1.2 7]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "-3.3",
},
{
name: "func min with unknown type in list",
value: `[[min 1 "b" -2]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedError: errMinUnknownType.Error(),
},
{
name: "func min with year expression",
value: `[[ min (now.Year) 2023 ]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "2023",
},
{
name: "func min with single value",
value: `[[ min 2023 ]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "2023",
},
{
name: "func min with no arguments",
value: `[[min]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedError: errMinWrongNoOfArgs.Error(),
},
{
name: "func sha1 hmac Hex",
value: `[[hmac "sha1" "secret" "string1" "string2"]]`,
Expand Down