diff --git a/collect.go b/collect.go index c40dbc4..c3c9576 100644 --- a/collect.go +++ b/collect.go @@ -31,116 +31,124 @@ type Collector struct { } func collector(logger log.Logger) *Collector { + var labels = []string{"user_name"} + if *byCategoryMetrics { + labels = append(labels, "category") + } + return &Collector{ logger: logger, blocks: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "blocks"), "blocks", - []string{"user_name"}, + labels, nil, ), bounceDrops: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "bounce_drops"), "bounce_drops", - []string{"user_name"}, + labels, nil, ), bounces: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "bounces"), "bounces", - []string{"user_name"}, + labels, nil, ), clicks: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "clicks"), "clicks", - []string{"user_name"}, + labels, nil, ), deferred: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "deferred"), "deferred", - []string{"user_name"}, + labels, nil, ), delivered: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "delivered"), "delivered", - []string{"user_name"}, + labels, nil, ), invalidEmails: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "invalid_emails"), "invalid_emails", - []string{"user_name"}, + labels, nil, ), opens: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "opens"), "opens", - []string{"user_name"}, + labels, nil, ), processed: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "processed"), "processed", - []string{"user_name"}, + labels, nil, ), requests: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "requests"), "requests", - []string{"user_name"}, + labels, nil, ), spamReportDrops: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "spam_report_drops"), "spam_report_drops", - []string{"user_name"}, + labels, nil, ), spamReports: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "spam_reports"), "spam_reports", - []string{"user_name"}, + labels, nil, ), uniqueClicks: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "unique_clicks"), "unique_clicks", - []string{"user_name"}, + labels, nil, ), uniqueOpens: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "unique_opens"), "unique_opens", - []string{"user_name"}, + labels, nil, ), unsubscribeDrops: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "unsubscribe_drops"), "unsubscribe_drops", - []string{"user_name"}, + labels, nil, ), unsubscribes: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "unsubscribes"), "unsubscribes", - []string{"user_name"}, + labels, nil, ), } } func (c *Collector) Collect(ch chan<- prometheus.Metric) { - var today time.Time + var today time.Time = time.Now() if *location != "" && *timeOffset != 0 { loc := time.FixedZone(*location, *timeOffset) today = time.Now().In(loc) - } else { - today = time.Now() + } else if *location != "" { + loc, err := time.LoadLocation(*location) + if err == nil { + today = time.Now().In(loc) + } } queryDate := today @@ -148,109 +156,231 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric) { queryDate = now.With(today).BeginningOfMonth() } - statistics, err := collectByDate(queryDate, today) - if err != nil { - level.Error(c.logger).Log(err) + if !*byCategoryMetrics { + statistics, err := collectByDate(queryDate, today) + if err != nil { + level.Error(c.logger).Log(err) + return + } - return - } + for _, stats := range statistics[0].Stats { + ch <- prometheus.MustNewConstMetric( + c.blocks, + prometheus.GaugeValue, + float64(stats.Metrics.Blocks), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.bounceDrops, + prometheus.GaugeValue, + float64(stats.Metrics.BounceDrops), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.bounces, + prometheus.GaugeValue, + float64(stats.Metrics.Bounces), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.clicks, + prometheus.GaugeValue, + float64(stats.Metrics.Clicks), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.deferred, + prometheus.GaugeValue, + float64(stats.Metrics.Deferred), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.delivered, + prometheus.GaugeValue, + float64(stats.Metrics.Delivered), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.invalidEmails, + prometheus.GaugeValue, + float64(stats.Metrics.InvalidEmails), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.opens, + prometheus.GaugeValue, + float64(stats.Metrics.Opens), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.processed, + prometheus.GaugeValue, + float64(stats.Metrics.Processed), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.requests, + prometheus.GaugeValue, + float64(stats.Metrics.Requests), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.spamReportDrops, + prometheus.GaugeValue, + float64(stats.Metrics.SpamReportDrops), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.spamReports, + prometheus.GaugeValue, + float64(stats.Metrics.SpamReports), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.uniqueClicks, + prometheus.GaugeValue, + float64(stats.Metrics.UniqueClicks), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.uniqueOpens, + prometheus.GaugeValue, + float64(stats.Metrics.UniqueOpens), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.unsubscribeDrops, + prometheus.GaugeValue, + float64(stats.Metrics.UnsubscribeDrops), + *sendGridUserName, + ) + ch <- prometheus.MustNewConstMetric( + c.unsubscribes, + prometheus.GaugeValue, + float64(stats.Metrics.Unsubscribes), + *sendGridUserName, + ) + } + } else { + category_statistics, err := collectByCategory(today) - for _, stats := range statistics[0].Stats { - ch <- prometheus.MustNewConstMetric( - c.blocks, - prometheus.GaugeValue, - float64(stats.Metrics.Blocks), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.bounceDrops, - prometheus.GaugeValue, - float64(stats.Metrics.BounceDrops), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.bounces, - prometheus.GaugeValue, - float64(stats.Metrics.Bounces), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.clicks, - prometheus.GaugeValue, - float64(stats.Metrics.Clicks), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.deferred, - prometheus.GaugeValue, - float64(stats.Metrics.Deferred), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.delivered, - prometheus.GaugeValue, - float64(stats.Metrics.Delivered), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.invalidEmails, - prometheus.GaugeValue, - float64(stats.Metrics.InvalidEmails), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.opens, - prometheus.GaugeValue, - float64(stats.Metrics.Opens), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.processed, - prometheus.GaugeValue, - float64(stats.Metrics.Processed), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.requests, - prometheus.GaugeValue, - float64(stats.Metrics.Requests), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.spamReportDrops, - prometheus.GaugeValue, - float64(stats.Metrics.SpamReportDrops), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.spamReports, - prometheus.GaugeValue, - float64(stats.Metrics.SpamReports), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.uniqueClicks, - prometheus.GaugeValue, - float64(stats.Metrics.UniqueClicks), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.uniqueOpens, - prometheus.GaugeValue, - float64(stats.Metrics.UniqueOpens), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.unsubscribeDrops, - prometheus.GaugeValue, - float64(stats.Metrics.UnsubscribeDrops), - *sendGridUserName, - ) - ch <- prometheus.MustNewConstMetric( - c.unsubscribes, - prometheus.GaugeValue, - float64(stats.Metrics.Unsubscribes), - *sendGridUserName, - ) + if err != nil { + level.Error(c.logger).Log(err) + return + } + for _, stats := range category_statistics.Stats { + ch <- prometheus.MustNewConstMetric( + c.blocks, + prometheus.GaugeValue, + float64(stats.Metrics.Blocks), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.bounceDrops, + prometheus.GaugeValue, + float64(stats.Metrics.BounceDrops), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.bounces, + prometheus.GaugeValue, + float64(stats.Metrics.Bounces), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.clicks, + prometheus.GaugeValue, + float64(stats.Metrics.Clicks), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.deferred, + prometheus.GaugeValue, + float64(stats.Metrics.Deferred), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.delivered, + prometheus.GaugeValue, + float64(stats.Metrics.Delivered), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.invalidEmails, + prometheus.GaugeValue, + float64(stats.Metrics.InvalidEmails), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.opens, + prometheus.GaugeValue, + float64(stats.Metrics.Opens), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.processed, + prometheus.GaugeValue, + float64(stats.Metrics.Processed), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.requests, + prometheus.GaugeValue, + float64(stats.Metrics.Requests), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.spamReportDrops, + prometheus.GaugeValue, + float64(stats.Metrics.SpamReportDrops), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.spamReports, + prometheus.GaugeValue, + float64(stats.Metrics.SpamReports), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.uniqueClicks, + prometheus.GaugeValue, + float64(stats.Metrics.UniqueClicks), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.uniqueOpens, + prometheus.GaugeValue, + float64(stats.Metrics.UniqueOpens), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.unsubscribeDrops, + prometheus.GaugeValue, + float64(stats.Metrics.UnsubscribeDrops), + *sendGridUserName, + stats.Category, + ) + ch <- prometheus.MustNewConstMetric( + c.unsubscribes, + prometheus.GaugeValue, + float64(stats.Metrics.Unsubscribes), + *sendGridUserName, + stats.Category, + ) + } } } diff --git a/main.go b/main.go index a90ad7d..d487f0d 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "github.com/prometheus/client_golang/prometheus/collectors" "net/http" "os" "os/signal" @@ -11,6 +10,7 @@ import ( "github.com/go-kit/log/level" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/promlog" "github.com/prometheus/common/promlog/flag" @@ -57,6 +57,10 @@ var ( "sendgrid.accumulated-metrics", "[Optional] Accumulated SendGrid Metrics by month, to calculate monthly email limit.", ).Default("False").Envar("SENDGRID_ACCUMULATED_METRICS").Bool() + byCategoryMetrics = kingpin.Flag( + "sendgrid.by-category-metrics", + "[Optional] Collect SendGrid Metrics by category.", + ).Default("False").Envar("SENDGRID_BY_CATEGORY_METRICS").Bool() ) func main() { diff --git a/sendgrid.go b/sendgrid.go index b6caaaa..6f8b1eb 100644 --- a/sendgrid.go +++ b/sendgrid.go @@ -7,11 +7,13 @@ import ( "net/http" "net/url" "os" + "strconv" "time" ) const ( - endpoint = "https://api.sendgrid.com/v3/stats" + endpoint = "https://api.sendgrid.com/v3/stats" + endpoint_categories = "https://api.sendgrid.com/v3/categories/stats/sums" ) type Metrics struct { @@ -34,7 +36,8 @@ type Metrics struct { } type Stat struct { - Metrics *Metrics `json:"metrics,omitempty"` + Metrics *Metrics `json:"metrics,omitempty"` + Category string `json:"name,omitempty"` } type Statistics struct { @@ -92,3 +95,63 @@ func collectByDate(timeStart time.Time, timeEnd time.Time) ([]*Statistics, error return nil, fmt.Errorf("status code = %d, response = %s", res.StatusCode, res.Body) } } + +func collectByCategory(timeStart time.Time) (*Statistics, error) { + var limit int = 25 + var offset int = 0 + var stats *Statistics = &Statistics{} + + for { + parsedURL, err := url.Parse(endpoint_categories) + if err != nil { + return nil, err + } + layout := "2006-01-02" + dateStart := timeStart.Format(layout) + + query := url.Values{} + query.Set("start_date", dateStart) + query.Set("limit", strconv.Itoa(limit)) + query.Set("offset", strconv.Itoa(offset)) + if *accumulatedMetrics { + query.Set("aggregated_by", "month") + } else { + query.Set("aggregated_by", "day") + } + + parsedURL.RawQuery = query.Encode() + + req, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *sendGridAPIKey)) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var pageStats *Statistics + var reader io.Reader = res.Body + reader = io.TeeReader(reader, os.Stdout) + switch res.StatusCode { + case http.StatusTooManyRequests: + return nil, fmt.Errorf("API rate limit exceeded") + case http.StatusOK: + if err := json.NewDecoder(reader).Decode(&pageStats); err != nil { + return nil, err + } + stats.Stats = append(stats.Stats, pageStats.Stats...) + default: + return nil, fmt.Errorf("status code = %d, response = %s", res.StatusCode, res.Body) + } + if len(pageStats.Stats) < limit { + break + } + offset += limit + } + return stats, nil +}