From 673e88a09735158ccf8ab7c9f3f6ca163563db2f Mon Sep 17 00:00:00 2001 From: Filip Borkiewicz Date: Tue, 14 Nov 2023 04:23:04 -0600 Subject: [PATCH] Annotate twitter errors to make creating better alerts possible (#49) --- service/adapters/prometheus/prometheus.go | 47 ++++++++++++++++++++--- service/adapters/twitter/limiter.go | 4 +- service/adapters/twitter/twitter.go | 41 ++++++++++++++++++++ service/adapters/twitter/twitter_test.go | 19 +++++++++ 4 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 service/adapters/twitter/twitter_test.go diff --git a/service/adapters/prometheus/prometheus.go b/service/adapters/prometheus/prometheus.go index d2311cc..3af6b62 100644 --- a/service/adapters/prometheus/prometheus.go +++ b/service/adapters/prometheus/prometheus.go @@ -1,11 +1,13 @@ package prometheus import ( + "fmt" "runtime/debug" "time" "github.com/boreq/errors" "github.com/planetary-social/nos-crossposting-service/internal/logging" + "github.com/planetary-social/nos-crossposting-service/service/adapters/twitter" "github.com/planetary-social/nos-crossposting-service/service/app" "github.com/planetary-social/nos-crossposting-service/service/domain" "github.com/planetary-social/nos-crossposting-service/service/domain/accounts" @@ -32,6 +34,8 @@ const ( labelResultValueError = "error" labelResultValueInvalidPointerPassed = "invalidPointerPassed" + labelErrorDescription = "errorDescription" + labelAction = "action" labelActionValuePostTweet = "postTweet" labelActionValueGetUser = "getUser" @@ -64,14 +68,14 @@ func NewPrometheus(logger logging.Logger) (*Prometheus, error) { Name: "application_handler_calls_total", Help: "Total number of calls to application handlers.", }, - []string{labelHandlerName, labelResult}, + []string{labelHandlerName, labelResult, labelErrorDescription}, ) applicationHandlerCallDurationHistogram := prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "application_handler_calls_duration", Help: "Duration of calls to application handlers in seconds.", }, - []string{labelHandlerName, labelResult}, + []string{labelHandlerName, labelResult, labelErrorDescription}, ) subscriptionQueueLengthGauge := prometheus.NewGaugeVec( prometheus.GaugeOpts{ @@ -112,7 +116,7 @@ func NewPrometheus(logger logging.Logger) (*Prometheus, error) { Name: "twitter_api_calls", Help: "Total number of calls to Twitter API to post tweets.", }, - []string{labelResult, labelAction}, + []string{labelResult, labelAction, labelErrorDescription}, ) purplePagesLookupResultCounter := prometheus.NewCounterVec( prometheus.CounterOpts{ @@ -224,7 +228,8 @@ func (p *Prometheus) ReportRelayConnectionState(m map[domain.RelayAddress]app.Re func (p *Prometheus) ReportCallingTwitterAPIToPostATweet(err error) { labels := prometheus.Labels{ - labelAction: labelActionValuePostTweet, + labelAction: labelActionValuePostTweet, + labelErrorDescription: p.getTwitterErrorDescription(err), } if err == nil { labels[labelResult] = labelResultValueSuccess @@ -236,7 +241,8 @@ func (p *Prometheus) ReportCallingTwitterAPIToPostATweet(err error) { func (p *Prometheus) ReportCallingTwitterAPIToGetAUser(err error) { labels := prometheus.Labels{ - labelAction: labelActionValueGetUser, + labelAction: labelActionValueGetUser, + labelErrorDescription: p.getTwitterErrorDescription(err), } if err == nil { labels[labelResult] = labelResultValueSuccess @@ -278,6 +284,19 @@ func (p *Prometheus) ReportNumberOfLinkedPublicKeys(count int) { p.numberOfLinkedPublicKeysGauge.Set(float64(count)) } +func (p *Prometheus) getTwitterErrorDescription(err error) string { + if err == nil { + return "none" + } + + var twitterError twitter.TwitterError + if errors.As(err, &twitterError) { + return fmt.Sprintf("twitter/%s", twitterError.Description()) + } + + return "unknown" +} + type ApplicationCall struct { handlerName string p *Prometheus @@ -319,13 +338,31 @@ func (a *ApplicationCall) getLabels(err *error) prometheus.Labels { if err == nil { labels[labelResult] = labelResultValueInvalidPointerPassed + labels[labelErrorDescription] = "invalidPointer" } else { if *err == nil { labels[labelResult] = labelResultValueSuccess } else { labels[labelResult] = labelResultValueError } + labels[labelErrorDescription] = a.getErrorDescription(*err) } return labels } + +func (a *ApplicationCall) getErrorDescription(err error) string { + if err == nil { + return "none" + } + + if errors.Is(err, twitter.ErrExceededLimiterLimit) { + return "twitter/limiter" + } + + if errors.Is(err, twitter.TwitterError{}) { + return "twitter/error" + } + + return "unknown" +} diff --git a/service/adapters/twitter/limiter.go b/service/adapters/twitter/limiter.go index aeb1177..15f791e 100644 --- a/service/adapters/twitter/limiter.go +++ b/service/adapters/twitter/limiter.go @@ -6,6 +6,8 @@ import ( "github.com/boreq/errors" ) +var ErrExceededLimiterLimit = errors.New("exceeded the limit in limiter") + type Limiter struct { m map[string][]time.Time } @@ -24,7 +26,7 @@ func (l *Limiter) Limit(key string, number int, window time.Duration) error { } if len(l.m[key]) > number { - return errors.New("exceeded the limit") + return ErrExceededLimiterLimit } l.m[key] = append(l.m[key], time.Now()) diff --git a/service/adapters/twitter/twitter.go b/service/adapters/twitter/twitter.go index 223d398..b4a24a4 100644 --- a/service/adapters/twitter/twitter.go +++ b/service/adapters/twitter/twitter.go @@ -72,6 +72,7 @@ func (t *Twitter) PostTweet( response, err := client.CreateTweet(ctx, twitter.CreateTweetRequest{ Text: tweet.Text(), }) + err = t.convertError(err) t.metrics.ReportCallingTwitterAPIToPostATweet(err) if err != nil { t.logError(err) @@ -120,6 +121,7 @@ func (t *Twitter) GetAccountDetails( twitter.UserFieldProfileImageURL, }, }) + err = t.convertError(err) t.metrics.ReportCallingTwitterAPIToGetAUser(err) if err != nil { t.logError(err) @@ -159,6 +161,19 @@ func (t *Twitter) logError(err error) { } } +func (t *Twitter) convertError(err error) error { + if err == nil { + return nil + } + + var errorResponse *twitter.ErrorResponse + if errors.As(err, &errorResponse) { + return NewTwitterError(errorResponse) + } + + return err +} + type userAuthorizer struct { conf config.Config userAccessToken accounts.TwitterUserAccessToken @@ -195,3 +210,29 @@ func (a *userAuthorizer) Add(req *http.Request) { authHeader := auth.BuildOAuth1Header(req.Method, req.URL.String(), a.params) req.Header.Set("Authorization", authHeader) } + +type TwitterError struct { + underlying *twitter.ErrorResponse +} + +func NewTwitterError(underlying *twitter.ErrorResponse) TwitterError { + return TwitterError{underlying: underlying} +} + +func (t TwitterError) Error() string { + return fmt.Sprintf("twitter error: %s", t.underlying) +} + +func (t TwitterError) Unwrap() error { + return t.underlying +} + +func (t TwitterError) Description() string { + return t.underlying.Title +} + +func (t TwitterError) Is(target error) bool { + _, ok1 := target.(TwitterError) + _, ok2 := target.(*TwitterError) + return ok1 || ok2 +} diff --git a/service/adapters/twitter/twitter_test.go b/service/adapters/twitter/twitter_test.go new file mode 100644 index 0000000..edd5f2d --- /dev/null +++ b/service/adapters/twitter/twitter_test.go @@ -0,0 +1,19 @@ +package twitter_test + +import ( + "testing" + + "github.com/boreq/errors" + twitterlib "github.com/g8rswimmer/go-twitter/v2" + "github.com/planetary-social/nos-crossposting-service/service/adapters/twitter" + "github.com/stretchr/testify/require" +) + +func TestErrorIs(t *testing.T) { + someError := &twitterlib.ErrorResponse{} + err := twitter.NewTwitterError(someError) + require.ErrorIs(t, err, twitter.TwitterError{}) + require.ErrorIs(t, err, &twitter.TwitterError{}) + require.ErrorIs(t, errors.Wrap(err, "wrapped"), twitter.TwitterError{}) + require.ErrorIs(t, errors.Wrap(err, "wrapped"), &twitter.TwitterError{}) +}