Skip to content

Commit

Permalink
Expand fields to select labels
Browse files Browse the repository at this point in the history
Previously, fields could only select values located on the entry's
record. However, it's often necessary to filter or parse values that
would more naturally exist on fields like labels.

This commit refactors fields into an interface which is fulfilled by
both the `RecordField` and the `LabelField` objects. `RecordField` is
essentially what was previously called `Field`.

Notably, this change makes the zero-value of `Field` invalid, since it
will contain a `nil` pointer.
  • Loading branch information
camdencheek committed Jul 8, 2020
1 parent 5432688 commit 63a2352
Show file tree
Hide file tree
Showing 31 changed files with 1,029 additions and 599 deletions.
48 changes: 28 additions & 20 deletions docs/types/field.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
## Fields

_Fields_ are the primary way to tell carbon which fields of a log's record to use for the operations of its plugins.
_Fields_ are the primary way to tell carbon which fields of an entry to use for the operations of its plugins.
Most often, these will be things like fields to parse for a parser plugin, or the field to write a new value to.

Fields are `.`-delimited strings which allow you to selected into nested records in the field. The root level is specified by `$` such as in `$.key1`, but since all fields are expected to be relative to root, the `$` is implied and be omitted. For example, in the record below, `nested_key` can be equivalently selected with `$.key2.nested_key` or `key2.nested_key`.
Fields are `.`-delimited strings which allow you to select labels or records on the entry. Fields can currently be used to select labels or values on a record. To select a label, prefix your field with `$label.` such as with `$label.my_label`. For values on the record, use the prefix `$record.` such as `$record.my_value`.

```json
{
"key1": "value1",
"key2": {
"nested_key": "nested_value"
}
}
```
Record fields can be nested arbitrarily deeply, such as `$record.my_value.my_nested_value`.

If a field does not start with either `$label` or `$record`, `$record` is assumed. For example, `my_value` is equivalent to `$record.my_value`.

## Examples

Expand All @@ -27,20 +22,27 @@ Config:
- add:
field: "key3"
value: "value3"
- remove: "key2.nested_key1"
- remove: "$record.key2.nested_key1"
- add:
field: "$labels.my_label"
value: "my_label_value"
```
<table>
<tr><td> Input record </td> <td> Output record </td></tr>
<tr><td> Input entry </td> <td> Output entry </td></tr>
<tr>
<td>
```json
{
"key1": "value1",
"key2": {
"nested_key1": "nested_value1",
"nested_key2": "nested_value2"
"timestamp": "",
"labels": {},
"record": {
"key1": "value1",
"key2": {
"nested_key1": "nested_value1",
"nested_key2": "nested_value2"
}
}
}
```
Expand All @@ -50,11 +52,17 @@ Config:

```json
{
"key1": "value1",
"key2": {
"nested_key2": "nested_value2"
"timestamp": "",
"labels": {
"my_label": "my_label_value"
},
"key3": "value3"
"record": {
"key1": "value1",
"key2": {
"nested_key2": "nested_value2"
},
"key3": "value3"
}
}
```

Expand Down
10 changes: 5 additions & 5 deletions entry/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ func (entry *Entry) AddLabel(key, value string) {
entry.Labels[key] = value
}

func (entry *Entry) Get(field Field) (interface{}, bool) {
func (entry *Entry) Get(field FieldInterface) (interface{}, bool) {
return field.Get(entry)
}

func (entry *Entry) Set(field Field, val interface{}) {
field.Set(entry, val, true)
func (entry *Entry) Set(field FieldInterface, val interface{}) error {
return field.Set(entry, val)
}

func (entry *Entry) Delete(field Field) (interface{}, bool) {
func (entry *Entry) Delete(field FieldInterface) (interface{}, bool) {
return field.Delete(entry)
}

func (entry *Entry) Read(field Field, dest interface{}) error {
func (entry *Entry) Read(field FieldInterface, dest interface{}) error {
val, ok := entry.Get(field)
if !ok {
return fmt.Errorf("field does not exist")
Expand Down
78 changes: 65 additions & 13 deletions entry/entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,84 +35,84 @@ func TestRead(t *testing.T) {

t.Run("field not exist error", func(t *testing.T) {
var s string
err := testEntry.Read(Field{[]string{"nonexistant_field"}}, &s)
err := testEntry.Read(NewRecordField("nonexistant_field"), &s)
require.Error(t, err)
})

t.Run("unsupported type error", func(t *testing.T) {
var s **string
err := testEntry.Read(Field{[]string{"string_field"}}, &s)
err := testEntry.Read(NewRecordField("string_field"), &s)
require.Error(t, err)
})

t.Run("string", func(t *testing.T) {
var s string
err := testEntry.Read(Field{[]string{"string_field"}}, &s)
err := testEntry.Read(NewRecordField("string_field"), &s)
require.NoError(t, err)
require.Equal(t, "string_val", s)
})

t.Run("string error", func(t *testing.T) {
var s string
err := testEntry.Read(Field{[]string{"map_string_interface_field"}}, &s)
err := testEntry.Read(NewRecordField("map_string_interface_field"), &s)
require.Error(t, err)
})

t.Run("map[string]interface{}", func(t *testing.T) {
var m map[string]interface{}
err := testEntry.Read(Field{[]string{"map_string_interface_field"}}, &m)
err := testEntry.Read(NewRecordField("map_string_interface_field"), &m)
require.NoError(t, err)
require.Equal(t, map[string]interface{}{"nested": "interface_val"}, m)
})

t.Run("map[string]interface{} error", func(t *testing.T) {
var m map[string]interface{}
err := testEntry.Read(Field{[]string{"string_field"}}, &m)
err := testEntry.Read(NewRecordField("string_field"), &m)
require.Error(t, err)
})

t.Run("map[string]string from map[string]interface{}", func(t *testing.T) {
var m map[string]string
err := testEntry.Read(Field{[]string{"map_string_interface_field"}}, &m)
err := testEntry.Read(NewRecordField("map_string_interface_field"), &m)
require.NoError(t, err)
require.Equal(t, map[string]string{"nested": "interface_val"}, m)
})

t.Run("map[string]string from map[string]interface{} err", func(t *testing.T) {
var m map[string]string
err := testEntry.Read(Field{[]string{"map_string_interface_nonstring_field"}}, &m)
err := testEntry.Read(NewRecordField("map_string_interface_nonstring_field"), &m)
require.Error(t, err)
})

t.Run("map[string]string from map[interface{}]interface{}", func(t *testing.T) {
var m map[string]string
err := testEntry.Read(Field{[]string{"map_interface_interface_field"}}, &m)
err := testEntry.Read(NewRecordField("map_interface_interface_field"), &m)
require.NoError(t, err)
require.Equal(t, map[string]string{"nested": "interface_val"}, m)
})

t.Run("map[string]string from map[interface{}]interface{} nonstring key error", func(t *testing.T) {
var m map[string]string
err := testEntry.Read(Field{[]string{"map_interface_interface_nonstring_key_field"}}, &m)
err := testEntry.Read(NewRecordField("map_interface_interface_nonstring_key_field"), &m)
require.Error(t, err)
})

t.Run("map[string]string from map[interface{}]interface{} nonstring value error", func(t *testing.T) {
var m map[string]string
err := testEntry.Read(Field{[]string{"map_interface_interface_nonstring_value_field"}}, &m)
err := testEntry.Read(NewRecordField("map_interface_interface_nonstring_value_field"), &m)
require.Error(t, err)
})

t.Run("interface{} from any", func(t *testing.T) {
var i interface{}
err := testEntry.Read(Field{[]string{"map_interface_interface_field"}}, &i)
err := testEntry.Read(NewRecordField("map_interface_interface_field"), &i)
require.NoError(t, err)
require.Equal(t, map[interface{}]interface{}{"nested": "interface_val"}, i)
})

t.Run("string from []byte", func(t *testing.T) {
var i string
err := testEntry.Read(NewField("byte_field"), &i)
err := testEntry.Read(NewRecordField("byte_field"), &i)
require.NoError(t, err)
require.Equal(t, "test", i)
})
Expand All @@ -139,3 +139,55 @@ func TestCopy(t *testing.T) {
require.Equal(t, map[string]string{"label": "value"}, copy.Labels)
require.Equal(t, "test", copy.Record)
}

func TestFieldFromString(t *testing.T) {
cases := []struct {
name string
input string
output Field
expectedError bool
}{
{
"SimpleRecord",
"test",
Field{RecordField{[]string{"test"}}},
false,
},
{
"PrefixedRecord",
"$.test",
Field{RecordField{[]string{"test"}}},
false,
},
{
"FullPrefixedRecord",
"$record.test",
Field{RecordField{[]string{"test"}}},
false,
},
{
"SimpleLabel",
"$labels.test",
Field{LabelField{"test"}},
false,
},
{
"LabelsTooManyFields",
"$labels.test.bar",
Field{},
true,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
f, err := fieldFromString(tc.input)
if tc.expectedError {
require.Error(t, err)
return
}

require.Equal(t, tc.output, f)
})
}
}
Loading

0 comments on commit 63a2352

Please sign in to comment.