internal/fields/model.go (124 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. package fields import ( "fmt" "slices" "strings" "gopkg.in/yaml.v3" ) // FieldDefinition describes a single field with its properties. type FieldDefinition struct { Name string `yaml:"name"` Description string `yaml:"description"` Type string `yaml:"type"` ObjectType string `yaml:"object_type"` Value string `yaml:"value"` // The value to associate with a constant_keyword field. AllowedValues AllowedValues `yaml:"allowed_values"` ExpectedValues []string `yaml:"expected_values"` Pattern string `yaml:"pattern"` Unit string `yaml:"unit"` MetricType string `yaml:"metric_type"` External string `yaml:"external"` Index *bool `yaml:"index"` Enabled *bool `yaml:"enabled"` DocValues *bool `yaml:"doc_values"` Normalize []string `yaml:"normalize,omitempty"` Fields FieldDefinitions `yaml:"fields,omitempty"` MultiFields []FieldDefinition `yaml:"multi_fields,omitempty"` Reusable *ReusableConfig `yaml:"reusable,omitempty"` Runtime runtimeField `yaml:"runtime"` // disallowAtTopLevel transfers the reusability config from parent groups to nested fields. // It is negated respect to Reusable.TopLevel, so it is disabled by default. disallowAtTopLevel bool } type ReusableConfig struct { TopLevel bool `yaml:"top_level"` } // 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 } field.Name = name baseFields := cleanNested(&field) if len(baseFields) > 0 { // Some groups are used by convention in ECS to include // fields that can appear in the root level of the document. // Append their child fields directly instead. // Examples of such groups are `base` or `tracing`. fields = append(fields, baseFields...) if len(field.Fields) == 0 { // If it had base fields, and doesn't have any other // field, don't add it. continue } } fields = append(fields, field) } *fds = fields return nil case nilNode: *fds = nil return nil default: return fmt.Errorf("expected map or sequence") } } // cleanNested processes fields nested inside another field, and returns // defined base fields. // If a field name is prefixed by the parent field, this part is removed, // so the full path, taking into account the parent name, matches. // If a field name is not prefixed by the parent field, this is considered // a base field, that should appear at the top-level. It is removed from // the list of nested fields and returned as base field. func cleanNested(parent *FieldDefinition) (base []FieldDefinition) { var nested []FieldDefinition for _, field := range parent.Fields { if reusable := parent.Reusable; reusable != nil { field.disallowAtTopLevel = !reusable.TopLevel } // If the field name is prefixed by the name of its parent, // this is a normal nested field. If not, it is a base field. if strings.HasPrefix(field.Name, parent.Name+".") { field.Name = field.Name[len(parent.Name)+1:] nested = append(nested, field) } else { base = append(base, field) } } // At the moment of writing this code, a group field has base fields // (`base` and `tracing` groups), or nested fields, but not both. // This code handles the case of having groups with both kinds of fields, // just in case this happens. parent.Fields = nested return base } // AllowedValues is the list of allowed values for a field. type AllowedValues []AllowedValue // IsAllowed returns true if a given value is allowed. func (avs AllowedValues) IsAllowed(value string) bool { if len(avs) == 0 { // No configured allowed values, any value is allowed. return true } return slices.Contains(avs.Values(), value) } // Values returns the list of allowed values. func (avs AllowedValues) Values() []string { var values []string for _, v := range avs { values = append(values, v.Name) } return values } // ExpectedEventTypes returns the list of expected event types for a given value. func (avs AllowedValues) ExpectedEventTypes(value string) []string { for _, v := range avs { if v.Name == value { return v.ExpectedEventTypes } } // If we are here, IsAllowed(value) is also false. return nil } // AllowedValue is one of the allowed values for a field. type AllowedValue struct { Name string `yaml:"name"` Description string `yaml:"description"` ExpectedEventTypes []string `yaml:"expected_event_types"` }