Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 21 additions & 9 deletions internal/fields/dependency_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ const (
ecsSchemaName = "ecs"
gitReferencePrefix = "git@"

ecsSchemaFile = "fields.ecs.yml"
ecsSchemaURL = "https://raw.githubusercontent.com/elastic/ecs/%s/generated/beats/%s"
ecsSchemaFile = "ecs_nested.yml"
ecsSchemaURL = "https://raw.githubusercontent.com/elastic/ecs/%s/generated/ecs/%s"
)

// DependencyManager is responsible for resolving external field dependencies.
Expand Down Expand Up @@ -61,6 +61,15 @@ func loadECSFieldsSchema(dep buildmanifest.ECSDependency) ([]FieldDefinition, er
return nil, nil
}

content, err := readECSFieldsSchemaFile(dep)
if err != nil {
return nil, errors.Wrap(err, "error reading ECS fields schema file")
}

return parseECSFieldsSchema(content)
}

func readECSFieldsSchemaFile(dep buildmanifest.ECSDependency) ([]byte, error) {
gitReference, err := asGitReference(dep.Reference)
if err != nil {
return nil, errors.Wrap(err, "can't process the value as Git reference")
Expand All @@ -70,12 +79,8 @@ func loadECSFieldsSchema(dep buildmanifest.ECSDependency) ([]FieldDefinition, er
if err != nil {
return nil, errors.Wrap(err, "error fetching profile path")
}

cachedSchemaPath := filepath.Join(loc.FieldsCacheDir(), ecsSchemaName, gitReference, ecsSchemaFile)
content, err := os.ReadFile(cachedSchemaPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, errors.Wrapf(err, "can't read cached schema (path: %s)", cachedSchemaPath)
}
if errors.Is(err, os.ErrNotExist) {
logger.Debugf("Pulling ECS dependency using reference: %s", dep.Reference)

Expand Down Expand Up @@ -109,14 +114,21 @@ func loadECSFieldsSchema(dep buildmanifest.ECSDependency) ([]FieldDefinition, er
if err != nil {
return nil, errors.Wrapf(err, "can't write cached schema (path: %s)", cachedSchemaPath)
}
} else if err != nil {
return nil, errors.Wrapf(err, "can't read cached schema (path: %s)", cachedSchemaPath)
}

var f []FieldDefinition
err = yaml.Unmarshal(content, &f)
return content, nil
}

func parseECSFieldsSchema(content []byte) ([]FieldDefinition, error) {
var fields FieldDefinitions
err := yaml.Unmarshal(content, &fields)
if err != nil {
return nil, errors.Wrap(err, "unmarshalling field body failed")
}
return f[0].Fields, nil

return fields, nil
}

func asGitReference(reference string) (string, error) {
Expand Down
76 changes: 75 additions & 1 deletion internal/fields/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

package fields

import (
"fmt"
"strings"

"gopkg.in/yaml.v3"
)

// FieldDefinition describes a single field with its properties.
type FieldDefinition struct {
Name string `yaml:"name"`
Expand All @@ -16,7 +23,7 @@ type FieldDefinition struct {
External string `yaml:"external"`
Index *bool `yaml:"index"`
DocValues *bool `yaml:"doc_values"`
Fields []FieldDefinition `yaml:"fields,omitempty"`
Fields FieldDefinitions `yaml:"fields,omitempty"`
MultiFields []FieldDefinition `yaml:"multi_fields,omitempty"`
}

Expand Down Expand Up @@ -82,3 +89,70 @@ func updateFields(origFields, fields []FieldDefinition) []FieldDefinition {
}
return updatedFields
}

// FieldDefinitions is an array of FieldDefinition, this can be unmarshalled from
// a yaml list or a yaml map.
type FieldDefinitions []FieldDefinition

func (fds *FieldDefinitions) UnmarshalYAML(value *yaml.Node) error {
nilNode := yaml.Kind(0)
switch value.Kind {
case yaml.SequenceNode:
// Fields are defined as a list, this happens in Beats fields files.
var fields []FieldDefinition
err := value.Decode(&fields)
if err != nil {
return err
}
*fds = fields
return nil
case yaml.MappingNode:
// Fields are defined as a map, this happens in ecs fields files.
if len(value.Content)%2 != 0 {
return fmt.Errorf("pairs of key-values expected in map")
}
var fields []FieldDefinition
for i := 0; i+1 < len(value.Content); i += 2 {
key := value.Content[i]
value := value.Content[i+1]

var name string
err := key.Decode(&name)
if err != nil {
return err
}

var field FieldDefinition
err = value.Decode(&field)
if err != nil {
return err
}

// "base" group is used by convention in ECS to include
// fields that can appear in the root level of the document.
// Append its child fields directly instead.
if name == "base" {
fields = append(fields, field.Fields...)
} else {
field.Name = name
cleanNestedNames(field.Name, field.Fields)
fields = append(fields, field)
}
}
*fds = fields
return nil
case nilNode:
*fds = nil
return nil
default:
return fmt.Errorf("expected map or sequence")
}
}

func cleanNestedNames(parent string, fields []FieldDefinition) {
for i := range fields {
if strings.HasPrefix(fields[i].Name, parent+".") {
fields[i].Name = fields[i].Name[len(parent)+1:]
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
- name: destination.geo.location
external: ecs
- name: geo.location
external: ecs
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field is actually invalid, as reported in #750.

- name: source.geo.location
external: ecs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"lat": 1.0,
"lon": "2.0"
},
"geo.location.lat": 3.0,
"geo.location.lon": 4.0
"destination.geo.location.lat": 3.0,
"destination.geo.location.lon": 4.0
}
5 changes: 2 additions & 3 deletions test/packages/other/fields_tests/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ An example event for `first` looks as following:
"lat": 1.0,
"lon": "2.0"
},
"geo.location.lat": 3.0,
"geo.location.lon": 4.0
"destination.geo.location.lat": 3.0,
"destination.geo.location.lon": 4.0
}
```

Expand All @@ -22,5 +22,4 @@ An example event for `first` looks as following:
| data_stream.namespace | Data stream namespace. | constant_keyword |
| data_stream.type | Data stream type. | constant_keyword |
| destination.geo.location | Longitude and latitude. | geo_point |
| geo.location | Longitude and latitude. | geo_point |
| source.geo.location | Longitude and latitude. | geo_point |