Skip to content

Commit

Permalink
Merge pull request #9 from observIQ/field-enhancements
Browse files Browse the repository at this point in the history
Augment fields to allow selecting labels
  • Loading branch information
camdencheek authored Jul 8, 2020
2 parents 9f42edc + 388c90a commit cecee58
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 cecee58

Please sign in to comment.