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(&notifications)
   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  }