1// Copyright 2019 Prometheus Team 2// Licensed under the Apache License, Version 2.0 (the "License"); 3// you may not use this file except in compliance with the License. 4// You may obtain a copy of the License at 5// 6// http://www.apache.org/licenses/LICENSE-2.0 7// 8// Unless required by applicable law or agreed to in writing, software 9// distributed under the License is distributed on an "AS IS" BASIS, 10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11// See the License for the specific language governing permissions and 12// limitations under the License. 13 14package test 15 16import ( 17 "encoding/json" 18 "fmt" 19 "sync" 20 "testing" 21 "time" 22 23 "github.com/prometheus/alertmanager/api/v2/models" 24) 25 26// Collector gathers alerts received by a notification receiver 27// and verifies whether all arrived and within the correct time boundaries. 28type Collector struct { 29 t *testing.T 30 name string 31 opts *AcceptanceOpts 32 33 collected map[float64][]models.GettableAlerts 34 expected map[Interval][]models.GettableAlerts 35 36 mtx sync.RWMutex 37} 38 39func (c *Collector) String() string { 40 return c.name 41} 42 43// Collected returns a map of alerts collected by the collector indexed with the 44// receive timestamp. 45func (c *Collector) Collected() map[float64][]models.GettableAlerts { 46 c.mtx.RLock() 47 defer c.mtx.RUnlock() 48 return c.collected 49} 50 51func batchesEqual(as, bs models.GettableAlerts, opts *AcceptanceOpts) bool { 52 if len(as) != len(bs) { 53 return false 54 } 55 56 for _, a := range as { 57 found := false 58 for _, b := range bs { 59 if equalAlerts(a, b, opts) { 60 found = true 61 break 62 } 63 } 64 if !found { 65 return false 66 } 67 } 68 return true 69} 70 71// latest returns the latest relative point in time where a notification is 72// expected. 73func (c *Collector) latest() float64 { 74 c.mtx.RLock() 75 defer c.mtx.RUnlock() 76 var latest float64 77 for iv := range c.expected { 78 if iv.end > latest { 79 latest = iv.end 80 } 81 } 82 return latest 83} 84 85// Want declares that the Collector expects to receive the given alerts 86// within the given time boundaries. 87func (c *Collector) Want(iv Interval, alerts ...*TestAlert) { 88 c.mtx.Lock() 89 defer c.mtx.Unlock() 90 var nas models.GettableAlerts 91 for _, a := range alerts { 92 nas = append(nas, a.nativeAlert(c.opts)) 93 } 94 95 c.expected[iv] = append(c.expected[iv], nas) 96} 97 98// add the given alerts to the collected alerts. 99func (c *Collector) add(alerts ...*models.GettableAlert) { 100 c.mtx.Lock() 101 defer c.mtx.Unlock() 102 arrival := c.opts.relativeTime(time.Now()) 103 104 c.collected[arrival] = append(c.collected[arrival], models.GettableAlerts(alerts)) 105} 106 107func (c *Collector) Check() string { 108 report := fmt.Sprintf("\ncollector %q:\n\n", c) 109 110 c.mtx.RLock() 111 defer c.mtx.RUnlock() 112 for iv, expected := range c.expected { 113 report += fmt.Sprintf("interval %v\n", iv) 114 115 var alerts []models.GettableAlerts 116 for at, got := range c.collected { 117 if iv.contains(at) { 118 alerts = append(alerts, got...) 119 } 120 } 121 122 for _, exp := range expected { 123 found := len(exp) == 0 && len(alerts) == 0 124 125 report += fmt.Sprintf("---\n") 126 127 for _, e := range exp { 128 report += fmt.Sprintf("- %v\n", c.opts.alertString(e)) 129 } 130 131 for _, a := range alerts { 132 if batchesEqual(exp, a, c.opts) { 133 found = true 134 break 135 } 136 } 137 138 if found { 139 report += fmt.Sprintf(" [ ✓ ]\n") 140 } else { 141 c.t.Fail() 142 report += fmt.Sprintf(" [ ✗ ]\n") 143 } 144 } 145 } 146 147 // Detect unexpected notifications. 148 var totalExp, totalAct int 149 for _, exp := range c.expected { 150 for _, e := range exp { 151 totalExp += len(e) 152 } 153 } 154 for _, act := range c.collected { 155 for _, a := range act { 156 if len(a) == 0 { 157 c.t.Error("received empty notifications") 158 } 159 totalAct += len(a) 160 } 161 } 162 if totalExp != totalAct { 163 c.t.Fail() 164 report += fmt.Sprintf("\nExpected total of %d alerts, got %d", totalExp, totalAct) 165 } 166 167 if c.t.Failed() { 168 report += "\nreceived:\n" 169 170 for at, col := range c.collected { 171 for _, alerts := range col { 172 report += fmt.Sprintf("@ %v\n", at) 173 for _, a := range alerts { 174 report += fmt.Sprintf("- %v\n", c.opts.alertString(a)) 175 } 176 } 177 } 178 } 179 180 return report 181} 182 183// alertsToString returns a string representation of the given Alerts. Use for 184// debugging. 185func alertsToString(as []*models.GettableAlert) (string, error) { 186 b, err := json.Marshal(as) 187 if err != nil { 188 return "", err 189 } 190 191 return string(b), nil 192} 193 194// CompareCollectors compares two collectors based on their collected alerts 195func CompareCollectors(a, b *Collector, opts *AcceptanceOpts) (bool, error) { 196 f := func(collected map[float64][]models.GettableAlerts) []*models.GettableAlert { 197 result := []*models.GettableAlert{} 198 for _, batches := range collected { 199 for _, batch := range batches { 200 for _, alert := range batch { 201 result = append(result, alert) 202 } 203 } 204 } 205 return result 206 } 207 208 aAlerts := f(a.Collected()) 209 bAlerts := f(b.Collected()) 210 211 if len(aAlerts) != len(bAlerts) { 212 aAsString, err := alertsToString(aAlerts) 213 if err != nil { 214 return false, err 215 } 216 bAsString, err := alertsToString(bAlerts) 217 if err != nil { 218 return false, err 219 } 220 221 err = fmt.Errorf( 222 "first collector has %v alerts, second collector has %v alerts\n%v\n%v", 223 len(aAlerts), len(bAlerts), 224 aAsString, bAsString, 225 ) 226 return false, err 227 } 228 229 for _, aAlert := range aAlerts { 230 found := false 231 for _, bAlert := range bAlerts { 232 if equalAlerts(aAlert, bAlert, opts) { 233 found = true 234 break 235 } 236 } 237 238 if !found { 239 aAsString, err := alertsToString([]*models.GettableAlert{aAlert}) 240 if err != nil { 241 return false, err 242 } 243 bAsString, err := alertsToString(bAlerts) 244 if err != nil { 245 return false, err 246 } 247 248 err = fmt.Errorf( 249 "could not find matching alert for alert from first collector\n%v\nin alerts of second collector\n%v", 250 aAsString, bAsString, 251 ) 252 253 return false, err 254 } 255 } 256 257 return true, nil 258} 259