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

Add expr language for logs and use it in filter processor #5680

Closed
Closed
Show file tree
Hide file tree
Changes from 9 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
35 changes: 35 additions & 0 deletions internal/coreinternal/processor/filterexpr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Expr language

Expressions give the config flexibility by allowing dynamic business logic rules to be included in static configs.
jpkrohling marked this conversation as resolved.
Show resolved Hide resolved
Most notably, expressions can be used to filter logs and metrics based on content and properties of processed records.

For reference documentation of the expression language,
see [here](https://github.com/antonmedv/expr/blob/master/docs/Language-Definition.md).

## Metrics

For metrics the following variables as available:

| Name | [Type][type] | Description |
|------------|--------------|---------------------|
| MetricName | string | Name of the Metric. |

In addition the following functions are available:

| Name | Signature | Description |
|----------|-------------------------|--------------------------------------------------------------|
| HasLabel | func(key string) bool | Returns true if the Metric has given label, otherwise false. |
| Label | func(key string) string | Returns value of given label name if exists, otherwise `""` |

## Logs

For logs, the following variables are available:

| Name | [Type][type] | Description |
|----------------|--------------|------------------------------------|
| Body | string | Stringified Body of the LogRecord. |
| Name | string | Name of the LogRecord. |
| SeverityNumber | number | SeverityNumber of the LogRecord. |
| SeverityText | string | SeverityText of the LogRecord. |

[type]: https://github.com/antonmedv/expr/blob/master/docs/Language-Definition.md#supported-literals
75 changes: 75 additions & 0 deletions internal/coreinternal/processor/filterexpr/log_matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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 filterexpr

import (
"github.com/antonmedv/expr"
"github.com/antonmedv/expr/vm"
"go.opentelemetry.io/collector/model/pdata"
)

type LogMatcher struct {
program *vm.Program
v vm.VM
}

// logEnv is a structure of variables and functions for expr language
type logEnv struct {
Body string
Name string
SeverityNumber int32
SeverityText string
}

// NewLogMatcher creates new log matcher
func NewLogMatcher(expression string) (*LogMatcher, error) {
program, err := expr.Compile(expression)
if err != nil {
return nil, err
}
return &LogMatcher{program: program, v: vm.VM{}}, nil
}

// MatchLog returns true if log matches the matcher
func (m *LogMatcher) MatchLog(log pdata.LogRecord) (bool, error) {
matched, err := m.match(createLogEnv(log))
if err != nil {
return false, err
}

if matched {
return true, nil
}

return false, nil
}

// createLogEnv converts pdata.LogRecord to logEnv
func createLogEnv(log pdata.LogRecord) logEnv {
return logEnv{
Name: log.Name(),
Body: log.Body().AsString(),
SeverityNumber: int32(log.SeverityNumber()),
SeverityText: log.SeverityText(),
}
}

func (m *LogMatcher) match(env logEnv) (bool, error) {
result, err := m.v.Run(m.program, env)
if err != nil {
return false, err
}
return result.(bool), nil
}
120 changes: 120 additions & 0 deletions internal/coreinternal/processor/filterexpr/log_matcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// 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 filterexpr

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/model/pdata"
)

func TestLogCompileExprError(t *testing.T) {
_, err := NewMetricMatcher("")
require.Error(t, err)
}

func TestLogRunExprError(t *testing.T) {
matcher, err := NewMetricMatcher("foo")
require.NoError(t, err)
matched, _ := matcher.match(metricEnv{})
require.False(t, matched)
}

func TestExpression(t *testing.T) {
type testCase struct {
name string
expression string
expected bool
body pdata.AttributeValue
logName string
severity int32
severityText string
}

testCases := []testCase{
{
name: "match body",
expression: `Body matches 'my.log'`,
expected: true,
body: pdata.NewAttributeValueString("my.log"),
},
{
name: "do not match body",
expression: `Body matches 'my.log'`,
expected: false,
body: pdata.NewAttributeValueString("mys.log"),
},
{
name: "match name",
expression: `Name matches 'my l.g'`,
expected: true,
body: pdata.NewAttributeValueEmpty(),
logName: "my log",
},
{
name: "do not match name",
expression: `Name matches 'my l..g'`,
expected: false,
body: pdata.NewAttributeValueEmpty(),
logName: "my log",
},
{
name: "match severity",
expression: `SeverityNumber > 3`,
expected: true,
body: pdata.NewAttributeValueEmpty(),
severity: 5,
},
{
name: "do not match severity",
expression: `SeverityNumber <= 3`,
expected: false,
body: pdata.NewAttributeValueEmpty(),
severity: 5,
},
{
name: "match severity name",
expression: `SeverityText matches 'foo'`,
expected: true,
body: pdata.NewAttributeValueEmpty(),
severityText: "foo bar",
},
{
name: "match severity name",
expression: `SeverityText matches 'foos'`,
expected: false,
body: pdata.NewAttributeValueEmpty(),
severityText: "foo bar",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matcher, err := NewLogMatcher(tc.expression)
require.NoError(t, err)
l := pdata.NewLogRecord()
l.SetName(tc.logName)
tc.body.CopyTo(l.Body())
l.SetSeverityNumber(pdata.SeverityNumber(tc.severity))
l.SetSeverityText(tc.severityText)

matched, err := matcher.MatchLog(l)
assert.NoError(t, err)
assert.Equal(t, tc.expected, matched)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,27 @@ import (
"go.opentelemetry.io/collector/model/pdata"
)

type Matcher struct {
type MetricMatcher struct {
program *vm.Program
v vm.VM
}

type env struct {
type metricEnv struct {
MetricName string
// TODO: replace this with GetLabel func(key string) (string,bool)
HasLabel func(key string) bool
Label func(key string) string
}

func NewMatcher(expression string) (*Matcher, error) {
func NewMetricMatcher(expression string) (*MetricMatcher, error) {
program, err := expr.Compile(expression)
if err != nil {
return nil, err
}
return &Matcher{program: program, v: vm.VM{}}, nil
return &MetricMatcher{program: program, v: vm.VM{}}, nil
}

func (m *Matcher) MatchMetric(metric pdata.Metric) (bool, error) {
func (m *MetricMatcher) MatchMetric(metric pdata.Metric) (bool, error) {
metricName := metric.Name()
switch metric.DataType() {
case pdata.MetricDataTypeGauge:
Expand All @@ -54,7 +54,7 @@ func (m *Matcher) MatchMetric(metric pdata.Metric) (bool, error) {
}
}

func (m *Matcher) matchGauge(metricName string, gauge pdata.Gauge) (bool, error) {
func (m *MetricMatcher) matchGauge(metricName string, gauge pdata.Gauge) (bool, error) {
pts := gauge.DataPoints()
for i := 0; i < pts.Len(); i++ {
matched, err := m.matchEnv(metricName, pts.At(i).Attributes())
Expand All @@ -68,7 +68,7 @@ func (m *Matcher) matchGauge(metricName string, gauge pdata.Gauge) (bool, error)
return false, nil
}

func (m *Matcher) matchSum(metricName string, sum pdata.Sum) (bool, error) {
func (m *MetricMatcher) matchSum(metricName string, sum pdata.Sum) (bool, error) {
pts := sum.DataPoints()
for i := 0; i < pts.Len(); i++ {
matched, err := m.matchEnv(metricName, pts.At(i).Attributes())
Expand All @@ -82,7 +82,7 @@ func (m *Matcher) matchSum(metricName string, sum pdata.Sum) (bool, error) {
return false, nil
}

func (m *Matcher) matchDoubleHistogram(metricName string, histogram pdata.Histogram) (bool, error) {
func (m *MetricMatcher) matchDoubleHistogram(metricName string, histogram pdata.Histogram) (bool, error) {
pts := histogram.DataPoints()
for i := 0; i < pts.Len(); i++ {
matched, err := m.matchEnv(metricName, pts.At(i).Attributes())
Expand All @@ -96,12 +96,12 @@ func (m *Matcher) matchDoubleHistogram(metricName string, histogram pdata.Histog
return false, nil
}

func (m *Matcher) matchEnv(metricName string, attributes pdata.AttributeMap) (bool, error) {
return m.match(createEnv(metricName, attributes))
func (m *MetricMatcher) matchEnv(metricName string, attributes pdata.AttributeMap) (bool, error) {
return m.match(createMetricEnv(metricName, attributes))
}

func createEnv(metricName string, attributes pdata.AttributeMap) env {
return env{
func createMetricEnv(metricName string, attributes pdata.AttributeMap) metricEnv {
return metricEnv{
MetricName: metricName,
HasLabel: func(key string) bool {
_, ok := attributes.Get(key)
Expand All @@ -114,7 +114,7 @@ func createEnv(metricName string, attributes pdata.AttributeMap) env {
}
}

func (m *Matcher) match(env env) (bool, error) {
func (m *MetricMatcher) match(env metricEnv) (bool, error) {
result, err := m.v.Run(m.program, env)
if err != nil {
return false, err
Expand Down
Loading