| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package config_validator |
| |
| import ( |
| "config" |
| "config_parser" |
| "fmt" |
| "net/mail" |
| "regexp" |
| "strings" |
| "time" |
| |
| "google.golang.org/protobuf/proto" |
| ) |
| |
| // This file contains logic to validate lists of MetricDefinition protos. |
| |
| // Date format is yyyy/mm/dd. This is done by specifying in the desired format |
| // Mon Jan 2 15:04:05 MST 2006. See time module documentation. |
| const dateFormat = "2006/01/02" |
| |
| // Valid names are 1-64 characters long. They have the same syntactic requirements |
| // as a C variable name. |
| var validNameRegexp = regexp.MustCompile("^[a-zA-Z][_a-zA-Z0-9]{1,65}$") |
| |
| // Valid proto names are 1-64 characters long. They have the same syntactic |
| // requirements as a C variable name with the exceptions that they can include |
| // '.' for package names and '$' for '$team_name'. |
| var validProtoNameRegexp = regexp.MustCompile("^[$a-zA-Z][_.$a-zA-Z0-9]{1,64}[a-zA-Z0-9]$") |
| |
| // Validate a list of MetricDefinitions. |
| func validateConfiguredMetricDefinitions(metrics []*config.MetricDefinition) (err error) { |
| metricErrors := newValidationErrors("metric") |
| |
| metricIds := map[uint32]int{} |
| metricNames := map[string]bool{} |
| metricLookup := map[uint32]*config.MetricDefinition{} |
| for i, metric := range metrics { |
| if _, ok := metricIds[metric.Id]; ok { |
| metricErrors.addError(fmt.Sprintf("Metric %d", metric.Id), fmt.Errorf("there are two metrics with id=%v", metric.Id)) |
| continue |
| } |
| metricIds[metric.Id] = i |
| |
| if _, ok := metricNames[metric.MetricName]; ok { |
| metricErrors.addError(metric.MetricName, fmt.Errorf("there are two metrics with name=%v", metric.MetricName)) |
| continue |
| } |
| metricNames[metric.MetricName] = true |
| |
| metricLookup[metric.Id] = metric |
| |
| if err = validateMetricDefinition(*metric); err != nil { |
| metricErrors.addError(metric.MetricName, err) |
| } |
| |
| } |
| |
| replacementIds := map[uint32]string{} |
| for _, metric := range metrics { |
| if metric.ReplacementMetricId != 0 { |
| if s, ok := replacementIds[metric.ReplacementMetricId]; ok { |
| metricErrors.addError(metric.MetricName, fmt.Errorf("found metric with duplicate replacement_metric_id %s", s)) |
| } |
| replacementIds[metric.ReplacementMetricId] = metric.MetricName |
| } |
| if err = validateReplacementMetricID(*metric, metricLookup); err != nil { |
| metricErrors.addError(metric.MetricName, err) |
| } |
| } |
| |
| return metricErrors.err() |
| } |
| |
| // Validate a single MetricDefinition. |
| func validateMetricDefinition(m config.MetricDefinition) (err error) { |
| validationErrors := newValidationErrors("field") |
| if !validNameRegexp.MatchString(m.MetricName) { |
| validationErrors.addError("metric_name", fmt.Errorf("metric names must match the regular expression '%v'", validNameRegexp)) |
| } |
| |
| if m.Id == 0 { |
| validationErrors.addError("id", fmt.Errorf("metric ID of zero is not allowed. Please specify a positive metric ID")) |
| } |
| |
| if m.MetricType == config.MetricDefinition_UNSET { |
| validationErrors.addError("metric_type", fmt.Errorf("is not set")) |
| } |
| |
| if m.MetaData == nil { |
| validationErrors.addError("meta_data", fmt.Errorf("meta_data is not set, additionally, in meta_data: %v", validateMetadata(config.MetricDefinition_Metadata{}))) |
| } else if err := validateMetadata(*m.MetaData); err != nil { |
| validationErrors.addError("meta_data", err) |
| } |
| |
| if err := validateMetricUnits(m); err != nil { |
| validationErrors.addError("metric_units", err) |
| } |
| |
| if m.IntBuckets != nil && m.MetricType != config.MetricDefinition_INT_HISTOGRAM && m.MetricType != config.MetricDefinition_INTEGER_HISTOGRAM { |
| validationErrors.addError("int_buckets", fmt.Errorf("int_buckets can only be set for metrics for metric type INT_HISTOGRAM and INTEGER_HISTOGRAM")) |
| } |
| |
| if m.ProtoName != "" && m.MetricType != config.MetricDefinition_CUSTOM { |
| validationErrors.addError("proto_name", fmt.Errorf("proto_name can only be set for metrics for metric type CUSTOM")) |
| } |
| |
| if config_parser.Cobalt11MetricTypesSet[m.MetricType] { |
| if m.MetricSemantics == nil || len(m.MetricSemantics) == 0 { |
| validationErrors.addError("metric_semantics", fmt.Errorf("metric_semantics is required for this metric")) |
| } |
| } else { |
| if len(m.MetricSemantics) > 0 { |
| validationErrors.addError("metric_semantics", fmt.Errorf("metric_semantics can only be set for Cobalt 1.1 metrics")) |
| } |
| } |
| |
| if err := validateMetricDefinitionForType(m); err != nil { |
| validationErrors.addError("other", err) |
| } |
| |
| if m.MetricType != config.MetricDefinition_CUSTOM { |
| if err := validateMetricDimensions(m); err != nil { |
| validationErrors.addError("metric_dimensions", err) |
| } |
| } |
| |
| err = validationErrors.err() |
| if err != nil { |
| return err |
| } |
| |
| return validateReportDefinitions(m) |
| } |
| |
| func validateReplacementMetricID(m config.MetricDefinition, mapping map[uint32]*config.MetricDefinition) (err error) { |
| if m.ReplacementMetricId == 0 { |
| return nil |
| } |
| |
| if m.NoReplacementMetric != "" { |
| return fmt.Errorf("replacement_metric_id and no_replacement_metric cannot be set at the same time") |
| } |
| |
| replacement, ok := mapping[m.ReplacementMetricId] |
| if !ok { |
| return fmt.Errorf("replacement_metric_id must refer to an extant metric (metric %v doesn't exist)", m.ReplacementMetricId) |
| } |
| |
| if len(m.MetricDimensions) != len(replacement.MetricDimensions) { |
| return fmt.Errorf("replacement metric should have the same number of metric_dimensions (%d vs %d)", len(m.MetricDimensions), len(replacement.MetricDimensions)) |
| } |
| for i := range m.MetricDimensions { |
| if !proto.Equal(m.MetricDimensions[i], replacement.MetricDimensions[i]) { |
| return fmt.Errorf("replacement metric does not contain the same metric_dimensions (%v != %v)", m.MetricDimensions[i].Dimension, replacement.MetricDimensions[i].Dimension) |
| } |
| } |
| |
| switch m.MetricType { |
| case config.MetricDefinition_EVENT_OCCURRED, config.MetricDefinition_EVENT_COUNT: |
| if replacement.MetricType != config.MetricDefinition_OCCURRENCE && replacement.MetricType != config.MetricDefinition_STRING && replacement.MetricType != config.MetricDefinition_INTEGER { |
| return fmt.Errorf("metric of type %v must be mapped to one of (OCCURRENCE, STRING, INTEGER) event (found %v)", m.MetricType, replacement.MetricType) |
| } |
| if replacement.MetricType == config.MetricDefinition_OCCURRENCE || replacement.MetricType == config.MetricDefinition_INTEGER { |
| for _, report := range m.Reports { |
| if len(report.CandidateList) != 0 || len(report.CandidateFile) != 0 { |
| return fmt.Errorf("metrics mapped to OCCURRENCE must contain no candidate based reports (found %v) with candidate_list or candidate_file set", report.Id) |
| } |
| } |
| } |
| if replacement.MetricType == config.MetricDefinition_STRING { |
| for _, report := range m.Reports { |
| if len(report.CandidateList) == 0 && len(report.CandidateFile) == 0 { |
| return fmt.Errorf("report `%v`: metrics mapped to STRING must contain either candidate_list or candidate_file in all reports", report.Id) |
| } |
| } |
| } |
| case config.MetricDefinition_INT_HISTOGRAM: |
| if replacement.MetricType != config.MetricDefinition_INTEGER_HISTOGRAM { |
| return fmt.Errorf("metric of type INT_HISTOGRAM must be mapped to a metric of type INTEGER_HISTOGRAM (found %v)", replacement.MetricType) |
| } |
| for _, report := range m.Reports { |
| if len(report.CandidateList) != 0 || len(report.CandidateFile) != 0 { |
| return fmt.Errorf("metrics mapped to INTEGER_HISTOGRAM do not support candidate strings. Report %v contains one", report.Id) |
| } |
| } |
| |
| if m.IntBuckets != nil { |
| if !proto.Equal(m.IntBuckets, replacement.IntBuckets) { |
| return fmt.Errorf("replacement metric does not contain the same IntegerBuckets declaration") |
| } |
| } |
| case config.MetricDefinition_FRAME_RATE, config.MetricDefinition_MEMORY_USAGE, config.MetricDefinition_ELAPSED_TIME: |
| if replacement.MetricType != config.MetricDefinition_INTEGER { |
| return fmt.Errorf("metric of type %v must be mapped to a metric of type INTEGER (found %v)", m.MetricType, replacement.MetricType) |
| } |
| for _, report := range m.Reports { |
| if len(report.CandidateList) != 0 || len(report.CandidateFile) != 0 { |
| return fmt.Errorf("metrics mapped to INTEGER do not support candidate strings. Report %v contains one", report.Id) |
| } |
| } |
| case config.MetricDefinition_OCCURRENCE, config.MetricDefinition_INTEGER, config.MetricDefinition_INTEGER_HISTOGRAM, config.MetricDefinition_STRING: |
| return fmt.Errorf("cobalt 1.1 metrics should not contain a replacement_metric_id field") |
| case config.MetricDefinition_CUSTOM: |
| return fmt.Errorf("CUSTOM metrics do not support replacement_metric_id") |
| } |
| return nil |
| } |
| |
| // Validate a single instance of Metadata. |
| func validateMetadata(m config.MetricDefinition_Metadata) (err error) { |
| errors := []string{} |
| |
| if len(m.ExpirationDate) == 0 { |
| errors = append(errors, "expiration_date field is required") |
| } else { |
| var exp time.Time |
| exp, err = time.ParseInLocation(dateFormat, m.ExpirationDate, time.UTC) |
| if err != nil { |
| errors = append(errors, fmt.Sprintf("expiration_date (%v) field is invalid, it must use the yyyy/mm/dd format", m.ExpirationDate)) |
| } else { |
| // Date one year and one day from today. |
| maxExp := time.Now().AddDate(1, 0, 0) |
| if exp.After(maxExp) { |
| errors = append(errors, fmt.Sprintf("expiration_date must be no later than %v", maxExp.Format(dateFormat))) |
| } |
| } |
| } |
| |
| for _, o := range m.Owner { |
| if _, err = mail.ParseAddress(o); err != nil { |
| errors = append(errors, fmt.Sprintf("'%v' is not a valid email address in owner field", o)) |
| } |
| } |
| |
| if m.MaxReleaseStage == config.ReleaseStage_RELEASE_STAGE_NOT_SET { |
| errors = append(errors, "max_release_stage is required") |
| } |
| |
| if len(errors) > 0 { |
| return fmt.Errorf("%v", strings.Join(errors, ", ")) |
| } else { |
| return nil |
| } |
| } |
| |
| // Validate the event_codes and max_event_code fields. Do not invoke on CUSTOM metrics. |
| func validateMetricDimensions(m config.MetricDefinition) error { |
| if len(m.MetricDimensions) == 0 { |
| // A metric definition is allowed to not have any metric dimensions. |
| return nil |
| } |
| |
| cobalt10MetricType := !(config_parser.Cobalt11MetricTypesSet[m.MetricType]) |
| |
| if len(m.MetricDimensions) > 5 { |
| return fmt.Errorf("there can be at most 5 dimensions in metric_dimensions") |
| } |
| |
| seenNames := make(map[string]bool) |
| numOptions := 1 |
| for i, md := range m.MetricDimensions { |
| if md.Dimension == "" { |
| return fmt.Errorf("dimension %d has no 'dimension' name", i) |
| } |
| |
| if seenNames[md.Dimension] { |
| return fmt.Errorf("duplicate dimension name in metric_dimensions %s", md.Dimension) |
| } |
| seenNames[md.Dimension] = true |
| |
| name := md.Dimension |
| |
| seenCodeNames := make(map[string]bool) |
| for _, codeName := range md.EventCodes { |
| if seenCodeNames[codeName] { |
| return fmt.Errorf("duplicate event_code name in metric_dimensions %s", name) |
| } |
| seenCodeNames[codeName] = true |
| } |
| |
| for from, to := range md.EventCodeAliases { |
| if !seenCodeNames[from] { |
| if seenCodeNames[to] { |
| return fmt.Errorf("unknown event_code %s for alias %s: %s. Did you mean %s: %s?", from, from, to, to, from) |
| } else { |
| return fmt.Errorf("unknown event_code_alias: %s: %s", from, to) |
| } |
| } |
| if seenCodeNames[to] { |
| return fmt.Errorf("cannot have event_code_alias %s, when that name already exists in event_codes", to) |
| } |
| } |
| |
| if len(md.EventCodes) == 0 && md.MaxEventCode == 0 { |
| return fmt.Errorf("for metric dimension %v, you must either define max_event_code or explicitly define at least one event code", name) |
| } |
| if md.MaxEventCode != 0 { |
| for j := range md.EventCodes { |
| if j > md.MaxEventCode { |
| return fmt.Errorf("event index %v in metric_dimension %v is greater than max_event_code %v", j, name, md.MaxEventCode) |
| } |
| } |
| numOptions *= int(md.MaxEventCode + 1) |
| } else { |
| for j := range md.EventCodes { |
| if cobalt10MetricType && j >= 1024 && len(m.MetricDimensions) > 4 { |
| return fmt.Errorf("event index %v in metric_dimension %v is greater than 1023, which means that this metric cannot support more than 4 metric_dimensions", j, name) |
| } |
| if j >= 32768 { |
| return fmt.Errorf("event index %v in metric_dimension %v is is too large (greater than 32,767)", j, name) |
| } |
| } |
| numOptions *= len(md.EventCodes) |
| } |
| } |
| if cobalt10MetricType && numOptions >= 1024 { |
| return fmt.Errorf("metric_dimensions have too many possible representations: %v. May be no more than 1023", numOptions) |
| } |
| |
| return nil |
| } |
| |
| func validateMetricUnits(m config.MetricDefinition) error { |
| hasMetricUnits := m.MetricUnits != config.MetricUnits_METRIC_UNITS_OTHER |
| hasMetricUnitsOther := len(m.MetricUnitsOther) > 0 |
| if m.MetricType != config.MetricDefinition_INTEGER && m.MetricType != config.MetricDefinition_INTEGER_HISTOGRAM { |
| if hasMetricUnits || hasMetricUnitsOther { |
| return fmt.Errorf("metric_units and metric_units_other must be set only for INTEGER and INTEGER_HISTOGRAM metrics") |
| } |
| return nil |
| } |
| |
| if hasMetricUnits == hasMetricUnitsOther { |
| return fmt.Errorf("metrics of type INTEGER and INTEGER_HISTOGRAM must have exactly one of metric_units or metric_units_other specified") |
| } |
| return nil |
| } |
| |
| /////////////////////////////////////////////////////////////// |
| // Validation for specific metric types: |
| /////////////////////////////////////////////////////////////// |
| |
| func validateMetricDefinitionForType(m config.MetricDefinition) error { |
| switch m.MetricType { |
| case config.MetricDefinition_EVENT_OCCURRED: |
| return validateEventOccurred(m) |
| case config.MetricDefinition_INT_HISTOGRAM: |
| return validateIntHistogram(m) |
| case config.MetricDefinition_EVENT_COUNT: |
| return nil |
| case config.MetricDefinition_FRAME_RATE: |
| return nil |
| case config.MetricDefinition_MEMORY_USAGE: |
| return nil |
| case config.MetricDefinition_ELAPSED_TIME: |
| return nil |
| case config.MetricDefinition_CUSTOM: |
| return validateCustom(m) |
| case config.MetricDefinition_OCCURRENCE: |
| return nil |
| case config.MetricDefinition_INTEGER: |
| return nil |
| case config.MetricDefinition_INTEGER_HISTOGRAM: |
| return validateIntHistogram(m) |
| case config.MetricDefinition_STRING: |
| return validateString(m) |
| } |
| |
| return fmt.Errorf("Unknown MetricType: %v", m.MetricType) |
| } |
| |
| func validateEventOccurred(m config.MetricDefinition) error { |
| if len(m.MetricDimensions) == 0 || m.MetricDimensions[0].MaxEventCode == 0 { |
| return fmt.Errorf("no max_event_code specified for metric of type EVENT_OCCURRED") |
| } |
| |
| if len(m.MetricDimensions) > 1 { |
| return fmt.Errorf("too many metric dimensions specified for metric of type EVENT_OCCURRED") |
| } |
| |
| for i, md := range m.MetricDimensions { |
| if md.MaxEventCode == 0 { |
| return fmt.Errorf("no max_event_code specified for metric_dimension %v", i) |
| } |
| } |
| |
| return nil |
| } |
| |
| func validateIntHistogram(m config.MetricDefinition) error { |
| if m.IntBuckets == nil { |
| return fmt.Errorf("no int_buckets specified for metric of type INT_HISTOGRAM") |
| } |
| |
| // TODO(azani): Validate bucket definition. |
| |
| for _, r := range m.Reports { |
| if m.IntBuckets != nil && r.IntBuckets != nil { |
| return fmt.Errorf("for report %q: both report and metric contain int_buckets stanzas, the stanza in %q will be ignored", r.ReportName, r.ReportName) |
| } |
| } |
| |
| return nil |
| } |
| |
| func validateString(m config.MetricDefinition) error { |
| if m.StringCandidateFile == "" { |
| return fmt.Errorf("no string_candidate_file specified for metric of type STRING") |
| } |
| if m.StringBufferMax == 0 { |
| return fmt.Errorf("no string_buffer_max specified for metric of type STRING") |
| } |
| |
| return nil |
| } |
| |
| func validateCustom(m config.MetricDefinition) error { |
| if len(m.MetricDimensions) > 0 { |
| return fmt.Errorf("metric_dimensions must not be set for metrics of type CUSTOM") |
| } |
| |
| if m.ProtoName == "" { |
| return fmt.Errorf("missing proto_name. Proto names are required for metrics of type CUSTOM") |
| } |
| |
| if !validProtoNameRegexp.MatchString(m.ProtoName) { |
| return fmt.Errorf("Invalid proto_name. Proto names must match the regular expression '%v'", validProtoNameRegexp) |
| } |
| |
| return nil |
| } |