github.com/Financial-Times/publish-availability-monitor@v1.12.0/checks/publishCheck.go (about) 1 package checks 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "time" 8 9 "github.com/Financial-Times/go-logger/v2" 10 "github.com/Financial-Times/publish-availability-monitor/feeds" 11 "github.com/Financial-Times/publish-availability-monitor/httpcaller" 12 "github.com/Financial-Times/publish-availability-monitor/metrics" 13 ) 14 15 const DateLayout = time.RFC3339Nano 16 17 // PublishCheck performs an availability check on a piece of content, at a 18 // given endpoint, and returns whether the check was successful or not. 19 // Holds all the information necessary to check content availability 20 // at an endpoint, as well as store and send the results of the check. 21 type PublishCheck struct { 22 Metric metrics.PublishMetric 23 username string 24 password string 25 Threshold int 26 CheckInterval int 27 ResultSink chan metrics.PublishMetric 28 endpointSpecificChecks map[string]EndpointSpecificCheck 29 log *logger.UPPLogger 30 } 31 32 // NewPublishCheck returns a PublishCheck ready to perform a check for pm.UUID, at the pm.Endpoint. 33 func NewPublishCheck( 34 metric metrics.PublishMetric, 35 username, password string, 36 threshold, checkInterval int, 37 resultSink chan metrics.PublishMetric, 38 endpointSpecificChecks map[string]EndpointSpecificCheck, 39 log *logger.UPPLogger, 40 ) *PublishCheck { 41 return &PublishCheck{ 42 Metric: metric, 43 username: username, 44 password: password, 45 Threshold: threshold, 46 CheckInterval: checkInterval, 47 ResultSink: resultSink, 48 endpointSpecificChecks: endpointSpecificChecks, 49 log: log, 50 } 51 } 52 53 // DoCheck performs an availability check on a piece of content at a certain 54 // endpoint, applying endpoint-specific processing. 55 // Returns true if the content is available at the endpoint, false otherwise. 56 func (pc PublishCheck) DoCheck() (checkSuccessful, ignoreCheck bool) { 57 pc.log.Infof("Running check for %s\n", pc) 58 check := pc.endpointSpecificChecks[pc.Metric.Config.Alias] 59 if check == nil { 60 pc.log.Warnf("No check for %s", pc) 61 return false, false 62 } 63 64 return check.isCurrentOperationFinished(&pc) 65 } 66 67 func (pc PublishCheck) String() string { 68 return LoggingContextForCheck(pc.Metric.Config.Alias, pc.Metric.UUID, pc.Metric.Platform, pc.Metric.TID) 69 } 70 71 // EndpointSpecificCheck is the interface which determines the state of the operation we are currently checking. 72 type EndpointSpecificCheck interface { 73 // Returns the state of the operation and whether this check should be ignored 74 isCurrentOperationFinished(pc *PublishCheck) (operationFinished, ignoreCheck bool) 75 } 76 77 // ContentCheck implements the EndpointSpecificCheck interface to check operation 78 // status for the content endpoint. 79 type ContentCheck struct { 80 httpCaller httpcaller.Caller 81 } 82 83 func NewContentCheck(httpCaller httpcaller.Caller) ContentCheck { 84 return ContentCheck{httpCaller: httpCaller} 85 } 86 87 func (c ContentCheck) isCurrentOperationFinished(pc *PublishCheck) (operationFinished, ignoreCheck bool) { 88 pm := pc.Metric 89 url := pm.Endpoint.String() + pm.UUID 90 resp, err := c.httpCaller.DoCall(httpcaller.Config{ 91 URL: url, 92 Username: pc.username, 93 Password: pc.password, 94 TID: httpcaller.ConstructPamTID(pm.TID), 95 }) 96 if err != nil { 97 pc.log.WithError(err).Warnf("Error calling URL: [%v] for %s", url, pc) 98 return false, false 99 } 100 defer func() { 101 _ = resp.Body.Close() 102 }() 103 104 // if the article was marked as deleted, operation is finished when the 105 // article cannot be found anymore 106 if pm.IsMarkedDeleted { 107 pc.log.Infof("Content Marked deleted. Checking %s, status code [%v]", pc, resp.StatusCode) 108 return resp.StatusCode == 404, false 109 } 110 111 // if not marked deleted, operation isn't finished until status is 200 112 if resp.StatusCode != 200 { 113 if resp.StatusCode != 404 { 114 pc.log.Infof("Checking %s, status code [%v]", pc, resp.StatusCode) 115 } 116 return false, false 117 } 118 119 // if status is 200, we check the publishReference 120 // this way we can handle updates 121 data, err := io.ReadAll(resp.Body) 122 if err != nil { 123 pc.log.WithError(err).Warnf("Checking %s. Cannot read response", 124 LoggingContextForCheck(pm.Config.Alias, pm.UUID, pm.Platform, pm.TID)) 125 return false, false 126 } 127 128 var jsonResp map[string]interface{} 129 130 if err = json.Unmarshal(data, &jsonResp); err != nil { 131 pc.log.WithError(err).Warnf("Checking %s. Cannot unmarshal JSON response", 132 LoggingContextForCheck(pm.Config.Alias, pm.UUID, pm.Platform, pm.TID)) 133 return false, false 134 } 135 136 return isSamePublishEvent(jsonResp, pc) 137 } 138 139 // ContentNeo4jCheck implements the EndpointSpecificCheck interface to check operation 140 // status for the content endpoint. 141 type ContentNeo4jCheck struct { 142 httpCaller httpcaller.Caller 143 } 144 145 func NewContentNeo4jCheck(httpCaller httpcaller.Caller) ContentNeo4jCheck { 146 return ContentNeo4jCheck{httpCaller: httpCaller} 147 } 148 149 //nolint:unparam 150 func (c ContentNeo4jCheck) isCurrentOperationFinished(pc *PublishCheck) (operationFinished, ignoreCheck bool) { 151 pm := pc.Metric 152 url := pm.Endpoint.String() + pm.UUID 153 154 resp, err := c.httpCaller.DoCall(httpcaller.Config{ 155 URL: url, 156 Username: pc.username, 157 Password: pc.password, 158 TID: httpcaller.ConstructPamTID(pm.TID)}) 159 160 if err != nil { 161 pc.log.Warnf("Error calling URL: [%v] for %s", url, pc) 162 return false, false 163 } 164 165 defer func() { 166 _ = resp.Body.Close() 167 }() 168 169 // if the article was marked as deleted, operation is finished when the 170 // article cannot be found anymore 171 if pm.IsMarkedDeleted { 172 pc.log.Infof("Content Marked deleted. Checking %s, status code [%v]", pc, resp.StatusCode) 173 return resp.StatusCode == 404, false 174 } 175 176 // if not marked deleted, operation isn't finished until status is 200 177 if resp.StatusCode != 200 { 178 if resp.StatusCode != 404 { 179 pc.log.Infof("Checking %s, status code [%v]", pc, resp.StatusCode) 180 } 181 return false, false 182 } 183 184 // if status is 200, we check the publishReference 185 // this way we can handle updates 186 data, err := io.ReadAll(resp.Body) 187 if err != nil { 188 pc.log.WithError(err).Warnf("Checking %s. Cannot read response", 189 LoggingContextForCheck(pm.Config.Alias, pm.UUID, pm.Platform, pm.TID)) 190 return false, false 191 } 192 193 var jsonResp map[string]interface{} 194 195 err = json.Unmarshal(data, &jsonResp) 196 if err != nil { 197 pc.log.WithError(err).Warnf("Checking %s. Cannot unmarshal JSON response", 198 LoggingContextForCheck(pm.Config.Alias, pm.UUID, pm.Platform, pm.TID)) 199 return false, false 200 } 201 202 return pm.UUID == jsonResp["uuid"].(string), false 203 } 204 205 // NotificationsCheck implements the EndpointSpecificCheck interface to build the endpoint URL and 206 // to check the operation is present in the notification feed 207 type NotificationsCheck struct { 208 httpCaller httpcaller.Caller 209 subscribedFeeds map[string][]feeds.Feed 210 feedName string 211 } 212 213 func NewNotificationsCheck(httpCaller httpcaller.Caller, subscribedFeeds map[string][]feeds.Feed, feedName string) NotificationsCheck { 214 return NotificationsCheck{ 215 httpCaller: httpCaller, 216 subscribedFeeds: subscribedFeeds, 217 feedName: feedName, 218 } 219 } 220 221 func (n NotificationsCheck) isCurrentOperationFinished(pc *PublishCheck) (operationFinished, ignoreCheck bool) { 222 notifications := n.checkFeed(pc.Metric.UUID, pc.Metric.Platform) 223 for _, e := range notifications { 224 checkData := map[string]interface{}{"publishReference": e.PublishReference, "lastModified": e.LastModified} 225 operationFinished, ignoreCheck := isSamePublishEvent(checkData, pc) 226 if operationFinished || ignoreCheck { 227 return operationFinished, ignoreCheck 228 } 229 } 230 231 return false, n.shouldSkipCheck(pc) 232 } 233 234 func (n NotificationsCheck) shouldSkipCheck(pc *PublishCheck) bool { 235 pm := pc.Metric 236 if !pm.IsMarkedDeleted { 237 return false 238 } 239 url := pm.Endpoint.String() + "/" + pm.UUID 240 resp, err := n.httpCaller.DoCall(httpcaller.Config{URL: url, Username: pc.username, Password: pc.password, TID: httpcaller.ConstructPamTID(pm.TID)}) 241 if err != nil { 242 pc.log.WithError(err).Warnf("Checking %s. Error calling URL: [%v]", 243 LoggingContextForCheck(pm.Config.Alias, pm.UUID, pm.Platform, pm.TID), url) 244 return false 245 } 246 defer func() { 247 _ = resp.Body.Close() 248 }() 249 250 if resp.StatusCode != 200 { 251 return false 252 } 253 254 var notifications []feeds.Notification 255 err = json.NewDecoder(resp.Body).Decode(¬ifications) 256 if err != nil { 257 return false 258 } 259 //ignore check if there are no previous notifications for this UUID 260 if len(notifications) == 0 { 261 return true 262 } 263 264 return false 265 } 266 267 func (n NotificationsCheck) checkFeed(uuid string, envName string) []*feeds.Notification { 268 envFeeds, found := n.subscribedFeeds[envName] 269 if found { 270 for _, f := range envFeeds { 271 if f.FeedName() == n.feedName { 272 notifications := f.NotificationsFor(uuid) 273 return notifications 274 } 275 } 276 } 277 278 return []*feeds.Notification{} 279 } 280 281 func isSamePublishEvent(jsonContent map[string]interface{}, pc *PublishCheck) (operationFinished, ignoreCheck bool) { 282 pm := pc.Metric 283 if jsonContent["publishReference"] == pm.TID { 284 pc.log.Infof("Checking %s. Matched publish reference.", pc) 285 return true, false 286 } 287 288 // look for rapid-fire publishes 289 lastModifiedDate, ok := parseLastModifiedDate(jsonContent) 290 if ok { 291 if lastModifiedDate.After(pm.PublishDate) { 292 pc.log.Infof("Checking %s. Last modified date [%v] is after publish date [%v]", pc, lastModifiedDate, pm.PublishDate) 293 return false, true 294 } 295 if lastModifiedDate.Equal(pm.PublishDate) { 296 pc.log.Infof("Checking %s. Last modified date [%v] is equal to publish date [%v]", pc, lastModifiedDate, pm.PublishDate) 297 return true, false 298 } 299 pc.log.Infof("Checking %s. Last modified date [%v] is before publish date [%v]", pc, lastModifiedDate, pm.PublishDate) 300 } else { 301 pc.log.Warnf("The field 'lastModified' is not valid: [%v]. Skip checking rapid-fire publishes for %s.", jsonContent["lastModified"], pc) 302 } 303 304 return false, false 305 } 306 307 func parseLastModifiedDate(jsonContent map[string]interface{}) (*time.Time, bool) { 308 lastModifiedDateAsString, ok := jsonContent["lastModified"].(string) 309 if ok && lastModifiedDateAsString != "" { 310 lastModifiedDate, err := time.Parse(DateLayout, lastModifiedDateAsString) 311 return &lastModifiedDate, err == nil 312 } 313 return nil, false 314 } 315 316 func LoggingContextForCheck(checkType string, uuid string, environment string, transactionID string) string { 317 return fmt.Sprintf("environment=[%v], checkType=[%v], uuid=[%v], transaction_id=[%v]", environment, checkType, uuid, transactionID) 318 }