Skip to content

Commit

Permalink
Annotate twitter errors to make creating better alerts possible (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
boreq authored Nov 14, 2023
1 parent 489d05c commit 673e88a
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 6 deletions.
47 changes: 42 additions & 5 deletions service/adapters/prometheus/prometheus.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -32,6 +34,8 @@ const (
labelResultValueError = "error"
labelResultValueInvalidPointerPassed = "invalidPointerPassed"

labelErrorDescription = "errorDescription"

labelAction = "action"
labelActionValuePostTweet = "postTweet"
labelActionValueGetUser = "getUser"
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
}
4 changes: 3 additions & 1 deletion service/adapters/twitter/limiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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())
Expand Down
41 changes: 41 additions & 0 deletions service/adapters/twitter/twitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -120,6 +121,7 @@ func (t *Twitter) GetAccountDetails(
twitter.UserFieldProfileImageURL,
},
})
err = t.convertError(err)
t.metrics.ReportCallingTwitterAPIToGetAUser(err)
if err != nil {
t.logError(err)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
19 changes: 19 additions & 0 deletions service/adapters/twitter/twitter_test.go
Original file line number Diff line number Diff line change
@@ -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{})
}

0 comments on commit 673e88a

Please sign in to comment.