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 }