integration_test/metadata/integration_metadata.go (195 lines of code) (raw):

// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package metadata import ( "bytes" "fmt" "reflect" "regexp" "cloud.google.com/go/monitoring/apiv3/v2/monitoringpb" "github.com/go-playground/validator/v10" yaml "github.com/goccy/go-yaml" "go.uber.org/multierr" ) // MetricLabel encodes a specification of a metric label in the metrics backend. type MetricLabel struct { // The label name, for example state. Name string `yaml:"name" validate:"required"` // The label value pattern, as an RE2 regular expression. ValueRegex string `yaml:"value_regex" validate:"required"` // The description of the label. Description string `yaml:"description" validate:"excludesall=‘’“”"` // Annotations/footnotes about the label. Notes []string `yaml:"notes,omitempty" validate:"omitempty,unique"` } // MetricSpec encodes a specification of a metric in the metrics backend. type MetricSpec struct { // The metric type, for example workload.googleapis.com/apache.current_connections. Type string `yaml:"type" validate:"required"` // The value type, for example INT64. ValueType string `yaml:"value_type" validate:"required,oneof=BOOL INT64 DOUBLE STRING DISTRIBUTION"` // The kind, for example GAUGE. Kind string `yaml:"kind" validate:"required,oneof=GAUGE DELTA CUMULATIVE"` // The unit of the metric. Unit string `yaml:"unit"` // The description of the metric. Description string `yaml:"description" validate:"excludesall=‘’“”"` // The monitored resource, for example gce_instance. // Currently we only test with gce_instance. MonitoredResources []string `yaml:"monitored_resources,flow" validate:"required,gt=0,dive,oneof=gce_instance"` // Mapping of expected label keys to label specs. Labels []*MetricLabel `yaml:"labels,omitempty" validate:"omitempty,gt=0,unique=Name,dive"` // Annotations/footnotes about the metric. Notes []string `yaml:"notes,omitempty" validate:"omitempty,unique"` } // ExpectedMetric encodes a series of assertions about what data we expect // to see in the metrics backend. type ExpectedMetric struct { // The metric being described. MetricSpec `yaml:",inline"` // If Optional is true, the test for this metric will be skipped. Optional bool `yaml:"optional,omitempty" validate:"excluded_with=Representative"` // Exactly one metric in each expected_metrics.yaml must // have Representative set to true. This metric can be used // to test that the integration is enabled. Representative bool `yaml:"representative,omitempty" validate:"excluded_with=Optional,excluded_with=Platform"` // Exclusive metric to a particular kind of platform. Platform string `yaml:"platform,omitempty" validate:"excluded_with=Representative,omitempty,oneof=linux windows"` // A list of platforms that this metric is not available on. // Examples: debian-11. Not valid are linux,windows. UnavailableOn []string `yaml:"unavailable_on,omitempty" validate:"excluded_with=Representative"` } type LogField struct { Name string `yaml:"name" validate:"required"` ValueRegex string `yaml:"value_regex"` Type string `yaml:"type" validate:"required"` Description string `yaml:"description" validate:"excludesall=‘’“”"` Optional bool `yaml:"optional,omitempty"` // A list of platforms that this field is not available on. // Examples: debian-11. UnavailableOn []string `yaml:"unavailable_on,omitempty"` // Annotations/footnotes about the field. Notes []string `yaml:"notes,omitempty" validate:"omitempty,unique"` } type ExpectedLog struct { LogName string `yaml:"log_name" validate:"required"` Fields []*LogField `yaml:"fields" validate:"required,unique=Name,dive"` // Annotations/footnotes about the log. Notes []string `yaml:"notes,omitempty" validate:"omitempty,unique"` } type MinimumSupportedAgentVersion struct { Logging string `yaml:"logging" validate:"required_without=Metrics"` Metrics string `yaml:"metrics" validate:"required_without=Logging"` } type ConfigurationFields struct { Name string `yaml:"name" validate:"required"` Default string `yaml:"default"` Description string `yaml:"description" validate:"required,excludesall=‘’“”"` } type InputConfiguration struct { Type string `yaml:"type" validate:"required"` Fields []*ConfigurationFields `yaml:"fields" validate:"required,dive"` } type ConfigurationOptions struct { LogsConfiguration []*InputConfiguration `yaml:"logs" validate:"required_without=MetricsConfiguration,dive"` MetricsConfiguration []*InputConfiguration `yaml:"metrics" validate:"required_without=LogsConfiguration,dive"` } type ExpectedMetricsContainer struct { ExpectedMetrics []*ExpectedMetric `yaml:"expected_metrics" validate:"onetrue=Representative,unique=Type,dive"` } type GpuPlatform struct { Model string `yaml:"model" validate:"required"` Platforms []string `yaml:"platforms" validate:"required"` } type IntegrationMetadata struct { PublicUrl string `yaml:"public_url"` AppUrl string `yaml:"app_url" validate:"required,url"` ShortName string `yaml:"short_name" validate:"required,excludesall=‘’“”"` LongName string `yaml:"long_name" validate:"required,excludesall=‘’“”"` LogoPath string `yaml:"logo_path"` Description string `yaml:"description" validate:"required,excludesall=‘’“”"` ConfigurationOptions *ConfigurationOptions `yaml:"configuration_options" validate:"required"` ConfigureIntegration string `yaml:"configure_integration"` ExpectedLogs []*ExpectedLog `yaml:"expected_logs" validate:"unique=LogName,dive"` MinimumSupportedAgentVersion *MinimumSupportedAgentVersion `yaml:"minimum_supported_agent_version"` SupportedAppVersion []string `yaml:"supported_app_version" validate:"required,unique,min=1"` SupportedOperatingSystems string `yaml:"supported_operating_systems" validate:"required,oneof=linux windows linux_and_windows"` PlatformsToSkip []string `yaml:"platforms_to_skip"` GpuPlatforms []GpuPlatform `yaml:"gpu_platforms" validate:"dive"` RestartAfterInstall bool `yaml:"restart_after_install"` Troubleshoot string `yaml:"troubleshoot" validate:"excludesall=‘’“”"` ExpectedMetricsContainer `yaml:",inline"` } func UnmarshalAndValidate(fullPath string, data []byte, i interface{}) error { data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")) v := NewIntegrationMetadataValidator() // Note: Unmarshaler does not protect when only the key being declared. // https://github.com/goccy/go-yaml/issues/313 if err := yaml.UnmarshalWithOptions(data, i, yaml.DisallowUnknownField(), yaml.Validator(v)); err != nil { return fmt.Errorf("%s%w", fullPath, err) } return nil } func SliceContains(slice []string, toFind string) bool { for _, entry := range slice { if entry == toFind { return true } } return false } func NewIntegrationMetadataValidator() *validator.Validate { v := validator.New() _ = v.RegisterValidation("onetrue", func(fl validator.FieldLevel) bool { field := fl.Field() param := fl.Param() if param == "" { panic("onetrue must contain an argument") } switch field.Kind() { case reflect.Slice, reflect.Array: elem := field.Type().Elem() // Ignore the case where this field is not actually specified or is left empty. if field.Len() == 0 { return true } if elem.Kind() == reflect.Ptr { elem = elem.Elem() } sf, ok := elem.FieldByName(param) if !ok { panic(fmt.Sprintf("Invalid field name %s", param)) } if sfTyp := sf.Type; sfTyp.Kind() != reflect.Bool { panic(fmt.Sprintf("Field %s is %s, not bool", param, sfTyp)) } count := 0 for i := 0; i < field.Len(); i++ { if reflect.Indirect(field.Index(i)).FieldByName(param).Bool() { count++ } } return count == 1 default: panic(fmt.Sprintf("Invalid field type %T", field.Interface())) } }) return v } func AssertMetric(metric *ExpectedMetric, series *monitoringpb.TimeSeries) error { var err error if series.ValueType.String() != metric.ValueType { err = multierr.Append(err, fmt.Errorf("valueType: expected %s but got %s", metric.ValueType, series.ValueType.String())) } if series.MetricKind.String() != metric.Kind { err = multierr.Append(err, fmt.Errorf("kind: expected %s but got %s", metric.Kind, series.MetricKind.String())) } if !SliceContains(metric.MonitoredResources, series.Resource.Type) { err = multierr.Append(err, fmt.Errorf("unexpected monitored_resource: expected %v but got %s", metric.MonitoredResources, series.Resource.Type)) } err = multierr.Append(err, assertMetricLabels(metric, series)) if err != nil { return fmt.Errorf("%s: %w", metric.Type, err) } return nil } func assertMetricLabels(metric *ExpectedMetric, series *monitoringpb.TimeSeries) error { var err error // Only expected labels must be present expectedLabels := make(map[string]bool) for _, expectedLabel := range metric.Labels { expectedLabels[expectedLabel.Name] = true } for actualLabel, actualValue := range series.Metric.Labels { if !expectedLabels[actualLabel] { err = multierr.Append(err, fmt.Errorf("got unexpected label %q with value %q", actualLabel, actualValue)) } } // All expected labels must be present and match the given pattern for _, expectedLabel := range metric.Labels { actualValue, ok := series.Metric.Labels[expectedLabel.Name] if !ok { err = multierr.Append(err, fmt.Errorf("expected label not found: %s", expectedLabel)) continue } match, matchErr := regexp.MatchString(fmt.Sprintf("^(?:%s)$", expectedLabel.ValueRegex), actualValue) if matchErr != nil { err = multierr.Append(err, fmt.Errorf("error parsing pattern. label=%s, pattern=%s, err=%v", expectedLabel.Name, expectedLabel.ValueRegex, matchErr, )) } else if !match { err = multierr.Append(err, fmt.Errorf("error: label value does not match pattern. label=%s, pattern=%s, value=%s", expectedLabel.Name, expectedLabel.ValueRegex, actualValue, )) } } return err }