github.com/GoogleCloudPlatform/opentelemetry-operations-collector@v0.0.3-0.20230814143943-2c2416e3c759/integrationtest/hostmetrics_test.go (about)

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //	http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package integrationtest
    15  
    16  import (
    17  	"bytes"
    18  	"context"
    19  	"fmt"
    20  	"os"
    21  	"path/filepath"
    22  	"regexp"
    23  	"runtime"
    24  	"strings"
    25  	"testing"
    26  	"time"
    27  
    28  	"go.opentelemetry.io/collector/pdata/pcommon"
    29  	"go.opentelemetry.io/collector/pdata/pmetric"
    30  	"gopkg.in/yaml.v3"
    31  
    32  	"github.com/GoogleCloudPlatform/opentelemetry-operations-collector/service"
    33  )
    34  
    35  func TestAgentMetrics(t *testing.T) {
    36  	runTest(t, "agentmetrics-config.yaml", "agentmetrics-expected.yaml")
    37  }
    38  
    39  func TestHostmetrics(t *testing.T) {
    40  	runTest(t, "hostmetrics-config.yaml", "hostmetrics-expected.yaml")
    41  }
    42  
    43  func runTest(t *testing.T, configFile, expectationsFile string) {
    44  	testDir, err := os.Getwd()
    45  	if err != nil {
    46  		t.Fatal(err)
    47  	}
    48  	oldArgs := os.Args
    49  	os.Args = append(os.Args, fmt.Sprintf("--config=%s", filepath.Join(testDir, configFile)))
    50  	// Restore the original args.
    51  	t.Cleanup(func() { os.Args = oldArgs })
    52  
    53  	// Make a scratch directory and cd there so that otelopscol will write
    54  	// metrics.json to a scratch directory instead of into source control.
    55  	scratchDir, err := os.MkdirTemp("", "")
    56  	if err != nil {
    57  		t.Fatal(err)
    58  	}
    59  	if err := os.Chdir(scratchDir); err != nil {
    60  		t.Fatal(err)
    61  	}
    62  	// Restore the original working directory.
    63  	t.Cleanup(func() {
    64  		if err := os.Chdir(testDir); err != nil {
    65  			t.Fatal(err)
    66  		}
    67  	})
    68  
    69  	terminationTime := 4 * time.Second
    70  	ctx, cancel := context.WithTimeout(context.Background(), terminationTime)
    71  	defer cancel()
    72  
    73  	// Run the main function of otelopscol.
    74  	// It will self-terminate in terminationTime.
    75  	service.MainContext(ctx)
    76  
    77  	observed := loadObservedMetrics(t, filepath.Join(scratchDir, "metrics.json"))
    78  	expected := loadExpectedMetrics(t, filepath.Join(testDir, expectationsFile))
    79  
    80  	expectMetricsMatch(t, observed, expected)
    81  }
    82  
    83  // ExpectedMetric encodes a series of assertions about what data we expect
    84  // to see in the metrics backend.
    85  type ExpectedMetric struct {
    86  	// The metric name, for example system.network.connections.
    87  	Name string `yaml:"name"`
    88  	// List of operating systems that the given metric is limited to.
    89  	// An empty list means the metric is supported on all platforms.
    90  	OnlyOn []string `yaml:"only_on"`
    91  	// The value type, for example "Int".
    92  	ValueType string `yaml:"value_type"`
    93  	// The metric type, for example "Gauge".
    94  	Type string `yaml:"type"`
    95  	// Mapping of expected attribute keys to value patterns.
    96  	// Patterns are RE2 regular expressions.
    97  	Attributes map[string]string `yaml:"attributes"`
    98  	// Mapping of expected resource attribute keys to value patterns.
    99  	// Patterns are RE2 regular expressions.
   100  	ResourceAttributes map[string]string `yaml:"resource_attributes"`
   101  }
   102  
   103  func sliceContains(haystack []string, needle string) bool {
   104  	for _, s := range haystack {
   105  		if needle == s {
   106  			return true
   107  		}
   108  	}
   109  	return false
   110  }
   111  
   112  // loadExpectedMetrics reads the metrics expectations from the given path.
   113  func loadExpectedMetrics(t *testing.T, expectedMetricsPath string) map[string]ExpectedMetric {
   114  	data, err := os.ReadFile(expectedMetricsPath)
   115  	if err != nil {
   116  		t.Fatal(err)
   117  	}
   118  	var agentMetrics struct {
   119  		ExpectedMetrics []ExpectedMetric `yaml:"expected_metrics"`
   120  	}
   121  
   122  	decoder := yaml.NewDecoder(bytes.NewReader(data))
   123  	decoder.KnownFields(true) // Fail decoding on unknown fields.
   124  	if err := decoder.Decode(&agentMetrics); err != nil {
   125  		t.Fatal(err)
   126  	}
   127  
   128  	result := make(map[string]ExpectedMetric)
   129  	for i, expect := range agentMetrics.ExpectedMetrics {
   130  		if expect.Name == "" {
   131  			t.Fatalf("ExpectedMetrics[%v] missing required field 'Name'.", i)
   132  		}
   133  		if _, ok := result[expect.Name]; ok {
   134  			t.Fatalf("Found multiple ExpectedMetric entries with Name=%q", expect.Name)
   135  		}
   136  		if len(expect.OnlyOn) == 0 || sliceContains(expect.OnlyOn, runtime.GOOS) {
   137  			result[expect.Name] = expect
   138  		}
   139  	}
   140  
   141  	t.Logf("Loaded %v metrics expectations from %s", len(result), expectedMetricsPath)
   142  
   143  	if len(result) < 2 {
   144  		t.Fatalf("Unreasonably few (<2) expectations found. expectations=%v", result)
   145  	}
   146  	return result
   147  }
   148  
   149  // loadObservedMetrics reads the contents of metrics.json produced by
   150  // otelopscol and selects one of the lines from it, corresponding to a single
   151  // batch of exported metrics.
   152  func loadObservedMetrics(t *testing.T, metricsJSONPath string) pmetric.Metrics {
   153  	data, err := os.ReadFile(metricsJSONPath)
   154  	if err != nil {
   155  		t.Fatal(err)
   156  	}
   157  
   158  	lines := strings.Split(string(data), "\n")
   159  	if len(lines) < 2 {
   160  		t.Fatalf("Only found %v lines in %q, need at least 2", len(lines), metricsJSONPath)
   161  	}
   162  
   163  	// Take the second batch of data exported by otelopscol. Picking the first
   164  	// batch can hit problems with certain metrics like system.cpu.utilization,
   165  	// which don't appear in the first batch.
   166  	secondBatch := []byte(lines[1])
   167  
   168  	t.Logf("Found %v bytes of data at %s, selecting %v bytes", len(data), metricsJSONPath, len(secondBatch))
   169  
   170  	unmarshaller := &pmetric.JSONUnmarshaler{}
   171  	metrics, err := unmarshaller.UnmarshalMetrics(secondBatch)
   172  	if err != nil {
   173  		t.Fatal(err)
   174  	}
   175  	return metrics
   176  }
   177  
   178  // expectMetricsMatch checks that all the data in `observedMetrics` matches
   179  // the expectations configured in `expectedMetrics`.
   180  // Note that an individual metric can appear many times in `observedMetrics`
   181  // under different values of its resource attributes. In particular, the
   182  // process.* metrics have resource attributes and so they will appear N times,
   183  // where N is the number of process resources that were detected.
   184  func expectMetricsMatch(t *testing.T, observedMetrics pmetric.Metrics, expectedMetrics map[string]ExpectedMetric) {
   185  	// Holds the set of metrics that were seen somewhere in observedMetrics.
   186  	seen := make(map[string]bool)
   187  
   188  	resourceMetrics := observedMetrics.ResourceMetrics()
   189  	for i := 0; i < resourceMetrics.Len(); i++ {
   190  		scopeMetrics := resourceMetrics.At(i).ScopeMetrics()
   191  
   192  		resourceAttributes := resourceMetrics.At(i).Resource().Attributes() // Used below.
   193  		for j := 0; j < scopeMetrics.Len(); j++ {
   194  			metrics := scopeMetrics.At(j).Metrics()
   195  
   196  			for k := 0; k < metrics.Len(); k++ {
   197  				metric := metrics.At(k)
   198  
   199  				name := metric.Name()
   200  
   201  				seen[name] = true
   202  
   203  				expected, ok := expectedMetrics[name]
   204  				if !ok {
   205  					// It's debatable whether a new metric being introduced should cause
   206  					// this test to fail. We decided to fail the test in this case because
   207  					// otherwise, there's no incentive to add coverage for new metrics as
   208  					// they appear. If it proves onerous to keep fixing this test, we can
   209  					// remove this check and come up with some other way to keep this test
   210  					// up to date.
   211  					t.Errorf("Unexpected metric %q observed.", name)
   212  					continue
   213  				}
   214  
   215  				t.Run(name, func(t *testing.T) {
   216  					if metric.Type().String() != expected.Type {
   217  						t.Errorf("For metric %q, Type()=%v, want %v",
   218  							name,
   219  							metric.Type().String(),
   220  							expected.Type,
   221  						)
   222  					}
   223  
   224  					expectAttributesMatch(t, resourceAttributes, expected.ResourceAttributes, name, "resource attribute")
   225  
   226  					var dataPoints pmetric.NumberDataPointSlice
   227  					switch metric.Type() {
   228  					case pmetric.MetricTypeGauge:
   229  						dataPoints = metric.Gauge().DataPoints()
   230  					case pmetric.MetricTypeSum:
   231  						dataPoints = metric.Sum().DataPoints()
   232  					default:
   233  						t.Errorf("Unimplemented handling for metric type %v", metric.Type())
   234  					}
   235  
   236  					for i := 0; i < dataPoints.Len(); i++ {
   237  						dataPoint := dataPoints.At(i)
   238  						expectAttributesMatch(t, dataPoint.Attributes(), expected.Attributes, name, "attribute")
   239  					}
   240  
   241  					expectValueTypesMatch(t, dataPoints, expected.ValueType, name)
   242  				})
   243  			}
   244  		}
   245  	}
   246  
   247  	// Don't forget to check that we saw all the metrics we expected!
   248  	for name := range expectedMetrics {
   249  		if _, ok := seen[name]; !ok {
   250  			t.Errorf("Never saw metric with name %q", name)
   251  		}
   252  	}
   253  }
   254  
   255  // expectValueTypesMatch checks that all data points in `dataPoints` have the
   256  // expected ValueType().
   257  func expectValueTypesMatch(t *testing.T, dataPoints pmetric.NumberDataPointSlice, expectedValueType, metricName string) {
   258  	for i := 0; i < dataPoints.Len(); i++ {
   259  		dataPoint := dataPoints.At(i)
   260  
   261  		newValueType := dataPoint.ValueType().String()
   262  		if newValueType != expectedValueType {
   263  			t.Errorf("For metric %q, ValueType()=%v, want %v",
   264  				metricName,
   265  				newValueType,
   266  				expectedValueType,
   267  			)
   268  		}
   269  	}
   270  }
   271  
   272  // expectAttributesMatch checks that the given pcommon.Map matches
   273  // the attributes and value regexes from expectedAttributes. Specifically,
   274  //  1. observedAttributes and expectedAttributes must have the same exact
   275  //     set of keys, and
   276  //  2. all values in observedAttributes[attr] must match the regex in
   277  //     expectedAttributes[attr].
   278  //
   279  // The `kind` argument should either be "attribute" or "resource attribute"
   280  // and is only used to generate appropriate error messages about what kind
   281  // of attribute is not matching.
   282  func expectAttributesMatch(t *testing.T, observedAttributes pcommon.Map, expectedAttributes map[string]string, metricName, kind string) {
   283  	// Iterate over observedAttributes, checking that:
   284  	// 1. Every attribute in observedAttributes is expected
   285  	// 2. Every attribute values in observedAttributes matches the regular
   286  	//    expression stored in expectedAttributes.
   287  	observedAttributes.Range(func(attribute string, pValue pcommon.Value) bool {
   288  		observedValue := pValue.AsString()
   289  		expectedPattern, ok := expectedAttributes[attribute]
   290  		if !ok {
   291  			t.Errorf("For metric %q, unexpected %s %q with value=%q",
   292  				metricName,
   293  				kind,
   294  				attribute,
   295  				observedValue,
   296  			)
   297  			return true // Tell Range() to keep going.
   298  		}
   299  		match, matchErr := regexp.MatchString(fmt.Sprintf("^(?:%s)$", expectedPattern), observedValue)
   300  		if matchErr != nil {
   301  			t.Errorf("Error parsing pattern. metric=%s, attribute=%s, pattern=%s, err=%v",
   302  				metricName,
   303  				attribute,
   304  				expectedPattern,
   305  				matchErr,
   306  			)
   307  		} else if !match {
   308  			t.Errorf("Value does not match pattern. metric=%s, %s=%s, pattern=%s, value=%s",
   309  				metricName,
   310  				kind,
   311  				attribute,
   312  				expectedPattern,
   313  				observedValue,
   314  			)
   315  		}
   316  		return true // Tell Range() to keep going.
   317  	})
   318  
   319  	// Check that all expected attributes actually appear in observedAttributes.
   320  	for attribute := range expectedAttributes {
   321  		if _, ok := observedAttributes.Get(attribute); !ok {
   322  			t.Errorf("For metric %q, missing expected %s %q. Found: %v",
   323  				metricName,
   324  				kind,
   325  				attribute,
   326  				keys(observedAttributes))
   327  			continue
   328  		}
   329  	}
   330  }
   331  
   332  // keys returns a slice containing just the keys from the input pcommon.Map m.
   333  func keys(m pcommon.Map) []string {
   334  	ks := make([]string, m.Len())
   335  	i := 0
   336  	m.Range(func(k string, _ pcommon.Value) bool {
   337  		ks[i] = k
   338  		i++
   339  		return true // Tell Range() to keep going.
   340  	})
   341  	return ks
   342  }