integration_test/cmd/generate_expected_metrics/generate_expected_metrics.go (212 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.
/*
PROJECT: The GCP project to use.
GOOGLE_APPLICATION_CREDENTIALS: Path to a credentials file for interacting with
GCP Cloud Monitoring services.
SCRIPTS_DIR: Path containing scripts for installing/configuring the various
applications and agents. Also has some files that aren't technically
scripts that tell the test what to do, such as supported_applications.txt.
FILTER: An optional Cloud Monitoring filter to use when querying for updated
metrics descriptors. If omitted, the script will pull all metric descriptors
using a set of default filters; see the defaultFilters variable.
FILTER is useful when testing a single integration, for example,
FILTER='metric.type=starts_with("workload.googleapis.com/apache")'
*/
package main
import (
"context"
"fmt"
"log"
"os"
"path"
"regexp"
"sort"
"time"
"github.com/GoogleCloudPlatform/ops-agent/integration_test/metadata"
monitoring "cloud.google.com/go/monitoring/apiv3"
"cloud.google.com/go/monitoring/apiv3/v2/monitoringpb"
"go.uber.org/multierr"
"google.golang.org/api/iterator"
"google.golang.org/genproto/googleapis/api/metric"
"gopkg.in/yaml.v2"
)
var (
monClient *monitoring.MetricClient
project = os.Getenv("PROJECT")
scriptsDir = os.Getenv("SCRIPTS_DIR")
filter = os.Getenv("FILTER")
defaultFilters = []string{
`metric.type = starts_with("workload.googleapis.com/")`,
`metric.type = starts_with("agent.googleapis.com/iis/")`,
`metric.type = starts_with("agent.googleapis.com/mssql/")`,
}
)
type expectedMetricsMap map[string]*metadata.ExpectedMetric
func main() {
if err := run(); err != nil {
log.Fatalf("%v", err)
}
}
func run() error {
if err := initMonitoringClient(); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
allMetrics, err := listAllMetricsByApp(ctx, project)
if err != nil {
return err
}
for app, newMetrics := range allMetrics {
log.Printf("Processing %d metrics for %s...\n", len(newMetrics), app)
existingMetrics, readErr := readExpectedMetrics(app)
if readErr != nil {
err = multierr.Append(err, readErr)
continue
}
// For each new metric, either update the corresponding existing metric,
// or add it.
for _, newMetric := range newMetrics {
existingMetrics[newMetric.Type] = updateMetric(existingMetrics[newMetric.Type], newMetric)
}
err = multierr.Append(err, writeExpectedMetrics(app, existingMetrics))
}
return err
}
// Initialize the global monitoring client.
func initMonitoringClient() error {
ctx := context.Background()
var err error
monClient, err = monitoring.NewMetricClient(ctx)
return err
}
// listMetrics calls projects.metricDescriptors.list with the given project ID and filter.
func listMetrics(ctx context.Context, project string, filter string) ([]*metric.MetricDescriptor, error) {
req := &monitoringpb.ListMetricDescriptorsRequest{
Name: "projects/" + project + "/metricDescriptors/",
Filter: filter,
}
it := monClient.ListMetricDescriptors(ctx, req)
metrics := make([]*metric.MetricDescriptor, 0)
for {
m, err := it.Next()
if err == iterator.Done {
break
} else if err != nil {
return nil, err
}
metrics = append(metrics, m)
}
return metrics, nil
}
// listAllMetrics calls projects.metricDescriptors.list with the given project ID
// using the Cloud Monitoring filter defined in FILTER, or an exhaustive set of
// default filters if FILTER is not defined. Metrics are returned as a map from
// app name to expectedMetricsMap.
func listAllMetricsByApp(ctx context.Context, project string) (map[string]expectedMetricsMap, error) {
metrics := make(map[string]expectedMetricsMap)
var err error
var filters []string
// User-defined FILTER takes priority over default filters
if len(filter) > 0 {
filters = []string{filter}
} else {
filters = defaultFilters
}
for _, filter := range filters {
listMetricsResult, listMetricsErr := listMetrics(ctx, project, filter)
if listMetricsErr != nil {
err = multierr.Append(err, listMetricsErr)
continue
}
for _, m := range listMetricsResult {
app := getAppName(m.Type)
if _, ok := metrics[app]; !ok {
metrics[app] = make(expectedMetricsMap)
} else if _, ok := metrics[app][m.Type]; ok {
err = multierr.Append(err, fmt.Errorf("duplicate metric found, skipping: %s", m.Type))
continue
}
metrics[app][m.Type] = toExpectedMetric(m)
}
}
return metrics, err
}
// getAppName parses out the app name from a metric type, for example:
//
// workload.googleapis.com/apache.xyz -> apache
// agent.googleapis.com/iis/xyz -> iis
func getAppName(metricType string) string {
matches := regexp.MustCompile(`.*\.googleapis.com\/([^/.]*)[/.].*`).FindStringSubmatch(metricType)
if len(matches) != 2 {
panic(fmt.Errorf("metric type doesn't match regex: %s", metricType))
}
app := matches[1]
if app == "" {
panic(fmt.Errorf("app not detected for metric type: %s", metricType))
}
return app
}
// toExpectedMetric converts from metric.MetricDescriptor to ExpectedMetric.
func toExpectedMetric(metric *metric.MetricDescriptor) *metadata.ExpectedMetric {
labels := make([]*metadata.MetricLabel, len(metric.Labels))
for _, l := range metric.Labels {
labels = append(labels, &metadata.MetricLabel{
Name: l.Key,
ValueRegex: ".*",
})
}
return &metadata.ExpectedMetric{
MetricSpec: metadata.MetricSpec{
Type: metric.Type,
Kind: metric.MetricKind.String(),
ValueType: metric.ValueType.String(),
MonitoredResources: []string{"gce_instance"},
Labels: labels,
},
}
}
func metadataFilename(app string) string {
return path.Join(scriptsDir, "applications", app, "metadata.yaml")
}
func readMetadata(app string) (metadata.IntegrationMetadata, error) {
file := metadataFilename(app)
serialized, err := os.ReadFile(file)
var metadata metadata.IntegrationMetadata
if err != nil {
return metadata, err
}
err = yaml.Unmarshal(serialized, &metadata)
return metadata, err
}
// readExpectedMetrics reads in metrics from the existing metadata.yaml
// file for the given app as a map keyed on metric type. If no metrics
// exist, an empty map is returned.
// Otherwise, its contents are returned, or an error if it could
// not be unmarshaled.
func readExpectedMetrics(app string) (expectedMetricsMap, error) {
metadata, err := readMetadata(app)
if err != nil {
return nil, err
}
metricsByType := make(expectedMetricsMap)
expectedMetrics := metadata.ExpectedMetrics
for _, m := range expectedMetrics {
if _, ok := metricsByType[m.Type]; ok {
return nil, fmt.Errorf("duplicate expected_metrics type in %s/metadata.yaml: %s", app, m.Type)
}
metricsByType[m.Type] = m
}
return metricsByType, nil
}
// writeExpectedMetrics writes the given map's values as a slice
// to the metadata.yaml associated with the given app. Metrics
// are written in alphabetical order by type.
func writeExpectedMetrics(app string, metrics expectedMetricsMap) error {
appMetadata, err := readMetadata(app)
if err != nil {
return err
}
expectedMetrics := make([]*metadata.ExpectedMetric, 0)
for _, m := range metrics {
metric := m
expectedMetrics = append(expectedMetrics, metric)
}
sort.Slice(expectedMetrics, func(i, j int) bool { return expectedMetrics[i].Type < expectedMetrics[j].Type })
appMetadata.ExpectedMetrics = expectedMetrics
serialized, err := yaml.Marshal(appMetadata)
if err != nil {
return err
}
return os.WriteFile(metadataFilename(app), serialized, 0644)
}
// updateMetric returns the given metric with updates applied from withValuesFrom.
// Existing Optional and Representative values are preserved, as well as existing
// label patterns. All other values are copied from withValuesFrom. Existing label
// keys not present in withValuesFrom.Labels are dropped.
// If toUpdate.Type is empty, then withValuesFrom is returned.
func updateMetric(toUpdate *metadata.ExpectedMetric, withValuesFrom *metadata.ExpectedMetric) *metadata.ExpectedMetric {
if toUpdate == nil || toUpdate.Type == "" {
// Empty struct to update; just copy over the new one
return withValuesFrom
}
if toUpdate.Type != withValuesFrom.Type {
panic(fmt.Errorf("updateMetric: attempted to update metric with mismatched type: %s, %s", toUpdate.Type, withValuesFrom.Type))
}
result := toUpdate
result.Kind = withValuesFrom.Kind
result.ValueType = withValuesFrom.ValueType
result.MonitoredResources = withValuesFrom.MonitoredResources
result.Labels = make([]*metadata.MetricLabel, len(withValuesFrom.Labels))
// TODO: Refactor to a simple map copy once we improve listMetrics to fetch
// label patterns automatically.
// The keys of result.Labels should be the same as withValuesFrom.Labels,
// except that existing values/patterns are preserved.
existingLabels := make(map[string]*metadata.MetricLabel)
for _, l := range toUpdate.Labels {
existingLabels[l.Name] = l
}
for _, l := range withValuesFrom.Labels {
existingLabel, ok := existingLabels[l.Name]
if ok {
// TODO: Merge the label specs.
result.Labels = append(result.Labels, existingLabel)
} else {
result.Labels = append(result.Labels, l)
}
}
return result
}