diff --git a/backend.go b/backend.go index 659f690ee..5fa2b0422 100644 --- a/backend.go +++ b/backend.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/gomodule/redigo/redis" + "github.com/nyaruka/gocommon/aws/cwatch" "github.com/nyaruka/gocommon/httpx" "github.com/nyaruka/gocommon/urns" ) @@ -105,6 +106,9 @@ type Backend interface { // RedisPool returns the redisPool for this backend RedisPool() *redis.Pool + + // CloudWatch return the CloudWatch service for this backend + CloudWatch() *cwatch.Service } // Media is a resolved media object that can be used as a message attachment diff --git a/backends/rapidpro/backend.go b/backends/rapidpro/backend.go index f21800a3b..6d3b92b6e 100644 --- a/backends/rapidpro/backend.go +++ b/backends/rapidpro/backend.go @@ -19,12 +19,14 @@ import ( "sync" "time" - "github.com/aws/aws-sdk-go-v2/service/s3/types" + cwtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/gomodule/redigo/redis" "github.com/jmoiron/sqlx" "github.com/nyaruka/courier" "github.com/nyaruka/courier/queue" "github.com/nyaruka/gocommon/analytics" + "github.com/nyaruka/gocommon/aws/cwatch" "github.com/nyaruka/gocommon/aws/dynamo" "github.com/nyaruka/gocommon/aws/s3x" "github.com/nyaruka/gocommon/cache" @@ -70,6 +72,7 @@ type backend struct { rp *redis.Pool dynamo *dynamo.Service s3 *s3x.Service + cw *cwatch.Service channelsByUUID *cache.Local[courier.ChannelUUID, *Channel] channelsByAddr *cache.Local[courier.ChannelAddress, *Channel] @@ -190,6 +193,11 @@ func (b *backend) Start() error { return err } + b.cw, err = cwatch.NewService(b.config.AWSAccessKeyID, b.config.AWSSecretAccessKey, b.config.AWSRegion, b.config.CloudwatchNamespace, b.config.DeploymentID) + if err != nil { + return err + } + // check attachment bucket access if err := b.s3.Test(ctx, b.config.S3AttachmentsBucket); err != nil { log.Error("attachments bucket not accessible", "error", err) @@ -635,7 +643,7 @@ func (b *backend) SaveAttachment(ctx context.Context, ch courier.Channel, conten path := filepath.Join("attachments", strconv.FormatInt(int64(orgID), 10), filename[:4], filename[4:8], filename) - storageURL, err := b.s3.PutObject(ctx, b.config.S3AttachmentsBucket, path, contentType, data, types.ObjectCannedACLPublicRead) + storageURL, err := b.s3.PutObject(ctx, b.config.S3AttachmentsBucket, path, contentType, data, s3types.ObjectCannedACLPublicRead) if err != nil { return "", fmt.Errorf("error saving attachment to storage (bytes=%d): %w", len(data), err) } @@ -775,6 +783,30 @@ func (b *backend) Heartbeat() error { b.stats.redisWaitDuration = redisStats.WaitDuration b.stats.redisWaitCount = redisStats.WaitCount + dims := []cwtypes.Dimension{ + cwatch.Dimension("Host", b.config.InstanceID), + cwatch.Dimension("App", "courier"), + } + + metrics := []cwtypes.MetricDatum{ + cwatch.Datum("DBBusy", float64(dbStats.InUse), cwtypes.StandardUnitCount, dims...), + cwatch.Datum("DBIdle", float64(dbStats.Idle), cwtypes.StandardUnitCount, dims...), + cwatch.Datum("DBWaitMS", float64(dbWaitDurationInPeriod/time.Millisecond), cwtypes.StandardUnitMilliseconds, dims...), + cwatch.Datum("DBWaitCount", float64(dbWaitCountInPeriod), cwtypes.StandardUnitCount, dims...), + cwatch.Datum("RedisActive", float64(redisStats.ActiveCount), cwtypes.StandardUnitCount, dims...), + cwatch.Datum("RedisIdle", float64(redisStats.IdleCount), cwtypes.StandardUnitCount, dims...), + cwatch.Datum("RedisWaitMS", float64(redisWaitDurationInPeriod/time.Millisecond), cwtypes.StandardUnitMilliseconds, dims...), + cwatch.Datum("RedisWaitCount", float64(redisWaitCountInPeriod), cwtypes.StandardUnitCount, dims...), + cwatch.Datum("BulkQueue", float64(bulkSize), cwtypes.StandardUnitCount, dims...), + cwatch.Datum("PriorityQueue", float64(prioritySize), cwtypes.StandardUnitCount, dims...), + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + defer cancel() + if err = b.CloudWatch().Send(ctx, metrics...); err != nil { + slog.Error("error putting metrics", "error", err) + } + analytics.Gauge("courier.db_busy", float64(dbStats.InUse)) analytics.Gauge("courier.db_idle", float64(dbStats.Idle)) analytics.Gauge("courier.db_wait_ms", float64(dbWaitDurationInPeriod/time.Millisecond)) @@ -873,3 +905,8 @@ func (b *backend) Status() string { func (b *backend) RedisPool() *redis.Pool { return b.rp } + +// CloudWatch return the cloudwatch service +func (b *backend) CloudWatch() *cwatch.Service { + return b.cw +} diff --git a/backends/rapidpro/contact.go b/backends/rapidpro/contact.go index f73735aa5..d291e4b0d 100644 --- a/backends/rapidpro/contact.go +++ b/backends/rapidpro/contact.go @@ -10,6 +10,8 @@ import ( "time" "unicode/utf8" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" + "github.com/aws/aws-sdk-go/aws" "github.com/jmoiron/sqlx" "github.com/nyaruka/courier" "github.com/nyaruka/gocommon/analytics" @@ -219,6 +221,17 @@ func contactForURN(ctx context.Context, b *backend, org OrgID, channel *Channel, // log that we created a new contact to librato analytics.Gauge("courier.new_contact", float64(1)) + dims := []types.Dimension{ + {Name: aws.String("ChannelType"), Value: aws.String(string(channel.ChannelType()))}, + {Name: aws.String("App"), Value: aws.String("courier")}, + } + + b.cw.Send(ctx, types.MetricDatum{ + MetricName: aws.String("NewContact"), + Dimensions: dims, + Value: aws.Float64(float64(1)), + Unit: types.StandardUnitCount, + }) // and return it return contact, nil diff --git a/config.go b/config.go index 625573a2f..abc326828 100644 --- a/config.go +++ b/config.go @@ -7,6 +7,7 @@ import ( "log" "log/slog" "net" + "os" "strings" "github.com/nyaruka/courier/utils" @@ -29,6 +30,10 @@ type Config struct { AWSSecretAccessKey string `help:"secret access key to use for AWS services"` AWSRegion string `help:"region to use for AWS services, e.g. us-east-1"` + CloudwatchNamespace string `help:"the namespace to use for cloudwatch metrics"` + DeploymentID string `help:"the deployment identifier to use for metrics"` + InstanceID string `help:"the instance identifier to use for metrics"` + DynamoEndpoint string `help:"DynamoDB service endpoint, e.g. https://dynamodb.us-east-1.amazonaws.com"` DynamoTablePrefix string `help:"prefix to use for DynamoDB tables"` @@ -60,6 +65,7 @@ type Config struct { // NewDefaultConfig returns a new default configuration object func NewDefaultConfig() *Config { + hostname, _ := os.Hostname() return &Config{ Backend: "rapidpro", Domain: "localhost", @@ -73,6 +79,10 @@ func NewDefaultConfig() *Config { AWSSecretAccessKey: "", AWSRegion: "us-east-1", + CloudwatchNamespace: "Temba/Courier", + DeploymentID: "dev", + InstanceID: hostname, + DynamoEndpoint: "", // let library generate it DynamoTablePrefix: "Temba", diff --git a/go.mod b/go.mod index 7707f78d8..369fcd81c 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,11 @@ go 1.23 require ( github.com/antchfx/xmlquery v1.4.2 - github.com/aws/aws-sdk-go-v2 v1.32.4 - github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.16 - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.0 - github.com/aws/aws-sdk-go-v2/service/s3 v1.67.0 + github.com/aws/aws-sdk-go-v2 v1.32.6 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.17 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0 github.com/buger/jsonparser v1.1.1 - github.com/dghubble/oauth1 v0.7.3 github.com/getsentry/sentry-go v0.29.1 github.com/go-chi/chi/v5 v5.1.0 github.com/golang-jwt/jwt/v5 v5.2.1 @@ -19,13 +18,13 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 github.com/nyaruka/ezconf v0.3.0 - github.com/nyaruka/gocommon v1.59.2 + github.com/nyaruka/gocommon v1.60.5 github.com/nyaruka/null/v3 v3.0.0 github.com/nyaruka/redisx v0.8.1 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/samber/slog-multi v1.2.4 github.com/samber/slog-sentry v1.2.2 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 golang.org/x/mod v0.22.0 golang.org/x/oauth2 v0.24.0 gopkg.in/go-playground/validator.v9 v9.31.0 @@ -34,30 +33,32 @@ require ( require ( cloud.google.com/go/compute/metadata v0.5.2 // indirect github.com/antchfx/xpath v1.3.2 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect - github.com/aws/aws-sdk-go-v2/config v1.28.4 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.45 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 // indirect + github.com/aws/aws-sdk-go v1.55.5 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect + github.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23 // indirect - github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.5 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.0 // indirect - github.com/aws/smithy-go v1.22.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.43.3 + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect + github.com/aws/smithy-go v1.22.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/structs v1.1.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.7 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect @@ -69,7 +70,7 @@ require ( github.com/naoina/toml v0.1.1 // indirect github.com/nyaruka/librato v1.1.1 // indirect github.com/nyaruka/null/v2 v2.0.3 // indirect - github.com/nyaruka/phonenumbers v1.4.2 // indirect + github.com/nyaruka/phonenumbers v1.4.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/samber/lo v1.47.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect diff --git a/go.sum b/go.sum index 5af72ee49..f2036b7f8 100644 --- a/go.sum +++ b/go.sum @@ -6,61 +6,63 @@ github.com/antchfx/xmlquery v1.4.2 h1:MZKd9+wblwxfQ1zd1AdrTsqVaMjMCwow3IqkCSe00K github.com/antchfx/xmlquery v1.4.2/go.mod h1:QXhvf5ldTuGqhd1SHNvvtlhhdQLks4dD0awIVhXIDTA= github.com/antchfx/xpath v1.3.2 h1:LNjzlsSjinu3bQpw9hWMY9ocB80oLOWuQqFvO6xt51U= github.com/antchfx/xpath v1.3.2/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= -github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE= -github.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA= -github.com/aws/aws-sdk-go-v2/config v1.28.4 h1:qgD0MKmkIzZR2DrAjWJcI9UkndjR+8f6sjUQvXh0mb0= -github.com/aws/aws-sdk-go-v2/config v1.28.4/go.mod h1:LgnWnNzHZw4MLplSyEGia0WgJ/kCGD86zGCjvNpehJs= -github.com/aws/aws-sdk-go-v2/credentials v1.17.45 h1:DUgm5lFso57E7150RBgu1JpVQoF8fAPretiDStIuVjg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.45/go.mod h1:dnBpENcPC1ekZrGpSWspX+ZRGzhkvqngT2Qp5xBR1dY= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.16 h1:JE5DYt99+qZSq0yYp8vF4g1KRgxanj1DiMVdG5lsN+k= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.16/go.mod h1:U3ZEr13jekqj6Nb/zVvGz+/Lhh4pZybtzjhIJy5aEmM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk= +github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= +github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4= +github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc= +github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0= +github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.17 h1:36xxDfD/hD9cMBjANIBSr+kZ0/+IYKHql4KPGN/DvM4= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.17/go.mod h1:A4XQVRy4yJ70Sk5Qz2tuCQX6J5kXcRa53nGP6wtgntM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23 h1:1SZBDiRzzs3sNhOMVApyWPduWYGAX0imGy06XiBnCAM= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23/go.mod h1:i9TkxgbZmHVh2S0La6CAXtnyFhlCX/pJ0JsOvBAS6Mk= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.0 h1:qgDx1ChCsz5tSxok9hxWES30bt4koYM1Xub4ONuNYDU= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.0/go.mod h1:P+1rrWglInpWvnBpN0pH8jIIhkLkBaolkRVG4X9Kous= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.5 h1:pc8+YeYe6bBe8D3QeBz9/S5kUZ9k9yoBMbljGIBMNK4= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.5/go.mod h1:R09/8/9eLYHJ50PQ8FlIGjZb3XA2t2XhcI5E5332eCI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4 h1:aaPpoG15S2qHkWm4KlEyF01zovK1nW4BBbyXuHNSE90= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4/go.mod h1:eD9gS2EARTKgGr/W5xwgY/ik9z/zqpW+m/xOQbVxrMk= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.4 h1:rWKH6IiWDRIxmsTJUB/wEY+EIPp+P3C78Vidl+HXp6w= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.4/go.mod h1:MzOAfuiNZ6asjVrA+dNvXl5lI2nmzXakSpDFLOcOyJ4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4 h1:E5ZAVOmI2apR8ADb72Q63KqwwwdW1XcMeXIlrZ1Psjg= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4/go.mod h1:wezzqVUOVVdk+2Z/JzQT4NxAU0NbhRe5W8pIE72jsWI= -github.com/aws/aws-sdk-go-v2/service/s3 v1.67.0 h1:SwaJ0w0MOp0pBTIKTamLVeTKD+iOWyNJRdJ2KCQRg6Q= -github.com/aws/aws-sdk-go-v2/service/s3 v1.67.0/go.mod h1:TMhLIyRIyoGVlaEMAt+ITMbwskSTpcGsCPDq91/ihY0= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 h1:HJwZwRt2Z2Tdec+m+fPjvdmkq2s9Ra+VR0hjF7V2o40= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 h1:zcx9LiGWZ6i6pjdcoE9oXAB6mUdeyC36Ia/QEiIvYdg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.0 h1:s7LRgBqhwLaxcocnAniBJp7gaAB+4I4vHzqUqjH18yc= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.0/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8= -github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= -github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 h1:JX70yGKLj25+lMC5Yyh8wBtvB01GDilyRuJvXJ4piD0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24/go.mod h1:+Ln60j9SUTD0LEwnhEB0Xhg61DHqplBrbZpLgyjoEHg= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.43.3 h1:nQLG9irjDGUFXVPDHzjCGEEwh0hZ6BcxTvHOod1YsP4= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.43.3/go.mod h1:URs8sqsyaxiAZkKP6tOEmhcs9j2ynFIomqOKY/CAHJc= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1 h1:vucMirlM6D+RDU8ncKaSZ/5dGrXNajozVwpmWNPn2gQ= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1/go.mod h1:fceORfs010mNxZbQhfqUjUeHlTwANmIT4mvHamuUaUg= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.6 h1:hIl7Z1zcfdzsl5SiV32acFj4gY/cZ5Xr9wd6PpoNYGE= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.6/go.mod h1:VswWf/9ztSHHnMP3SMtGqrFOooVXI6NTDNjTcyLQ2HY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 h1:gvZOjQKPxFXy1ft3QnEyXmT+IqneM9QAUWlM3r0mfqw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5/go.mod h1:DLWnfvIcm9IET/mmjdxeXbBKmTCm0ZB8p1za9BVteM8= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5 h1:3Y457U2eGukmjYjeHG6kanZpDzJADa2m0ADqnuePYVQ= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5/go.mod h1:CfwEHGkTjYZpkQ/5PvcbEtT7AJlG68KkEvmtwU8z3/U= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 h1:P1doBzv5VEg1ONxnJss1Kh5ZG/ewoIE4MQtKKc6Crgg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5/go.mod h1:NOP+euMW7W3Ukt28tAxPuoWao4rhhqJD3QEBk7oCg7w= +github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0 h1:Q2ax8S21clKOnHhhr933xm3JxdJebql+R7aNo7p7GBQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0/go.mod h1:ralv4XawHjEMaHOWnTFushl0WRqim/gQWesAMF6hTow= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dghubble/oauth1 v0.7.3 h1:EkEM/zMDMp3zOsX2DC/ZQ2vnEX3ELK0/l9kb+vs4ptE= -github.com/dghubble/oauth1 v0.7.3/go.mod h1:oxTe+az9NSMIucDPDCCtzJGsPhciJV33xocHfcR2sVY= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= -github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= +github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= +github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= github.com/getsentry/sentry-go v0.29.1 h1:DyZuChN8Hz3ARxGVV8ePaNXh1dQ7d76AiB117xcREwA= github.com/getsentry/sentry-go v0.29.1/go.mod h1:x3AtIzN01d6SiWkderzaH28Tm0lgkafpJ5Bm3li39O0= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= @@ -73,8 +75,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= -github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= @@ -115,16 +117,18 @@ github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8= github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= github.com/nyaruka/ezconf v0.3.0 h1:kGvJqVN8AHowb4HdaHAviJ0Z3yI5Pyekp1WqibFEaGk= github.com/nyaruka/ezconf v0.3.0/go.mod h1:89GUW6EPRNLIxT7lC4LWnjWTgZeQwRoX7lBmc8ralAU= -github.com/nyaruka/gocommon v1.59.2 h1:WHMM8O4w8K6lYwBBRPQxJr/wPdgnNgg6eq/gi1ogvVY= -github.com/nyaruka/gocommon v1.59.2/go.mod h1:0G8y8vhbsjkRhc614wYJ5cy9zRffy+a2+cv8ko4xB10= +github.com/nyaruka/gocommon v1.60.3 h1:fPQ9t6NX+mu7JQ7nXefgpBs8paqGvGXq3eA7VscsAVo= +github.com/nyaruka/gocommon v1.60.3/go.mod h1:kFJuOq8COneV7ssfK6xgCMJ8gP8fQifLQnNXBnE4YL0= +github.com/nyaruka/gocommon v1.60.5 h1:V10rosGzVArRspilfbi65TyHBZzjLQbwmaBeicr2Drw= +github.com/nyaruka/gocommon v1.60.5/go.mod h1:kFJuOq8COneV7ssfK6xgCMJ8gP8fQifLQnNXBnE4YL0= github.com/nyaruka/librato v1.1.1 h1:0nTYtJLl3Sn7lX3CuHsLf+nXy1k/tGV0OjVxLy3Et4s= github.com/nyaruka/librato v1.1.1/go.mod h1:fme1Fu1PT2qvkaBZyw8WW+SrnFe2qeeCWpvqmAaKAKE= github.com/nyaruka/null/v2 v2.0.3 h1:rdmMRQyVzrOF3Jff/gpU/7BDR9mQX0lcLl4yImsA3kw= github.com/nyaruka/null/v2 v2.0.3/go.mod h1:OCVeCkCXwrg5/qE6RU0c1oUVZBy+ZDrT+xYg1XSaIWA= github.com/nyaruka/null/v3 v3.0.0 h1:JvOiNuKmRBFHxzZFt4sWii+ewmMkCQ1vO7X0clTNn6E= github.com/nyaruka/null/v3 v3.0.0/go.mod h1:Sus286RmC8P0VihFuQDDQPib/xJQ7++TsaPLdRuwgVc= -github.com/nyaruka/phonenumbers v1.4.2 h1:V791/B74Sb5i/X7Od2AVKA0BkvcNaInf7DWykPS2YSU= -github.com/nyaruka/phonenumbers v1.4.2/go.mod h1:gv+CtldaFz+G3vHHnasBSirAi3O2XLqZzVWz4V1pl2E= +github.com/nyaruka/phonenumbers v1.4.3 h1:tR71UJ+DZu7TSkxoG8JI8HzHJkPD/m4KNiUX34Fvmlo= +github.com/nyaruka/phonenumbers v1.4.3/go.mod h1:gv+CtldaFz+G3vHHnasBSirAi3O2XLqZzVWz4V1pl2E= github.com/nyaruka/redisx v0.8.1 h1:d9Hc8nfSKTSEU+bx+YrB13d6bzAgiiHygk4jg/Q4nb4= github.com/nyaruka/redisx v0.8.1/go.mod h1:2TUmkDvprPInnmInR5AEbCm0zRRewkvSDVLsO+Do6iI= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= @@ -144,8 +148,8 @@ github.com/samber/slog-sentry v1.2.2/go.mod h1:bHm8jm1dks0p+xc/lH2i4TIFwnPcMTvZe github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= diff --git a/sender.go b/sender.go index 44a670a6e..cce334a14 100644 --- a/sender.go +++ b/sender.go @@ -7,8 +7,10 @@ import ( "log/slog" "time" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" "github.com/nyaruka/courier/utils/clogs" "github.com/nyaruka/gocommon/analytics" + "github.com/nyaruka/gocommon/aws/cwatch" "github.com/nyaruka/gocommon/urns" ) @@ -333,11 +335,22 @@ func (w *Sender) sendMessage(msg MsgOut) { secondDuration := float64(duration) / float64(time.Second) log.Debug("send complete", "status", status.Status(), "elapsed", duration) + channelTypeDim := cwatch.Dimension("ChannelType", string(msg.Channel().ChannelType())) + // report to librato if status.Status() == MsgStatusErrored || status.Status() == MsgStatusFailed { analytics.Gauge(fmt.Sprintf("courier.msg_send_error_%s", msg.Channel().ChannelType()), secondDuration) + backend.CloudWatch().Send( + sendCTX, + cwatch.Datum("MsgSendError", float64(secondDuration), types.StandardUnitSeconds, channelTypeDim), + ) + } else { analytics.Gauge(fmt.Sprintf("courier.msg_send_%s", msg.Channel().ChannelType()), secondDuration) + backend.CloudWatch().Send( + sendCTX, + cwatch.Datum("MsgSend", float64(secondDuration), types.StandardUnitSeconds, channelTypeDim), + ) } } diff --git a/server.go b/server.go index 11b90221a..105935cfa 100644 --- a/server.go +++ b/server.go @@ -17,10 +17,12 @@ import ( "sync" "time" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/nyaruka/courier/utils/clogs" "github.com/nyaruka/gocommon/analytics" + "github.com/nyaruka/gocommon/aws/cwatch" "github.com/nyaruka/gocommon/httpx" "github.com/nyaruka/gocommon/jsonx" ) @@ -317,10 +319,20 @@ func (s *server) channelHandleWrapper(handler ChannelHandler, handlerFunc Channe if channel != nil { // if we have a channel but no events were created, we still log this to analytics + channelTypeDim := cwatch.Dimension("ChannelType", string(channel.ChannelType())) + if len(events) == 0 { if hErr != nil { + s.Backend().CloudWatch().Send( + ctx, + cwatch.Datum("ChannelError", float64(secondDuration), types.StandardUnitSeconds, channelTypeDim), + ) analytics.Gauge(fmt.Sprintf("courier.channel_error_%s", channel.ChannelType()), secondDuration) } else { + s.Backend().CloudWatch().Send( + ctx, + cwatch.Datum("ChannelIgnored", float64(secondDuration), types.StandardUnitSeconds, channelTypeDim), + ) analytics.Gauge(fmt.Sprintf("courier.channel_ignored_%s", channel.ChannelType()), secondDuration) } } @@ -329,13 +341,25 @@ func (s *server) channelHandleWrapper(handler ChannelHandler, handlerFunc Channe switch e := event.(type) { case MsgIn: clog.SetAttached(true) + s.Backend().CloudWatch().Send( + ctx, + cwatch.Datum("MsgReceive", float64(secondDuration), types.StandardUnitSeconds, channelTypeDim), + ) analytics.Gauge(fmt.Sprintf("courier.msg_receive_%s", channel.ChannelType()), secondDuration) LogMsgReceived(r, e) case StatusUpdate: clog.SetAttached(true) + s.Backend().CloudWatch().Send( + ctx, + cwatch.Datum("MsgStatus", float64(secondDuration), types.StandardUnitSeconds, channelTypeDim), + ) analytics.Gauge(fmt.Sprintf("courier.msg_status_%s", channel.ChannelType()), secondDuration) LogMsgStatusReceived(r, e) case ChannelEvent: + s.Backend().CloudWatch().Send( + ctx, + cwatch.Datum("EventReceive", float64(secondDuration), types.StandardUnitSeconds, channelTypeDim), + ) analytics.Gauge(fmt.Sprintf("courier.evt_receive_%s", channel.ChannelType()), secondDuration) LogChannelEventReceived(r, e) } diff --git a/test/backend.go b/test/backend.go index 8d348265c..165eedc8a 100644 --- a/test/backend.go +++ b/test/backend.go @@ -13,6 +13,7 @@ import ( _ "github.com/lib/pq" "github.com/nyaruka/courier" "github.com/nyaruka/courier/utils" + "github.com/nyaruka/gocommon/aws/cwatch" "github.com/nyaruka/gocommon/httpx" "github.com/nyaruka/gocommon/urns" "github.com/nyaruka/gocommon/uuids" @@ -45,6 +46,8 @@ type MockBackend struct { mutex sync.RWMutex redisPool *redis.Pool + cw *cwatch.Service + writtenMsgs []courier.MsgIn writtenMsgStatuses []courier.StatusUpdate writtenChannelEvents []courier.ChannelEvent @@ -83,6 +86,11 @@ func NewMockBackend() *MockBackend { log.Fatal(err) } + CW, err := cwatch.NewService("root", "tembatemba", "us-east-1", "Temba", "test") + if err != nil { + log.Fatal(err) + } + return &MockBackend{ channels: make(map[courier.ChannelUUID]courier.Channel), channelsByAddress: make(map[courier.ChannelAddress]courier.Channel), @@ -91,6 +99,7 @@ func NewMockBackend() *MockBackend { sentMsgs: make(map[courier.MsgID]bool), seenExternalIDs: make(map[string]courier.MsgUUID), redisPool: redisPool, + cw: CW, } } @@ -382,6 +391,11 @@ func (mb *MockBackend) RedisPool() *redis.Pool { return mb.redisPool } +// CloudWatch returns the cloudwatch service for this backend +func (mb *MockBackend) CloudWatch() *cwatch.Service { + return mb.cw +} + //////////////////////////////////////////////////////////////////////////////// // Methods not part of the backed interface but used in tests ////////////////////////////////////////////////////////////////////////////////