diff --git a/cloudflare.go b/cloudflare.go index bce8f3c..dd55466 100644 --- a/cloudflare.go +++ b/cloudflare.go @@ -217,7 +217,17 @@ type zoneResp struct { OriginResponseStatus uint16 `json:"originResponseStatus"` ClientCountryName string `json:"clientCountryName"` ClientRequestHTTPHost string `json:"clientRequestHTTPHost"` + OriginIP string `json:"originIP"` } `json:"dimensions"` + Quantiles struct { + EdgeTimeToFirstByteMsP50 int `json:"edgeTimeToFirstByteMsP50"` + EdgeTimeToFirstByteMsP95 int `json:"edgeTimeToFirstByteMsP95"` + EdgeTimeToFirstByteMsP99 int `json:"edgeTimeToFirstByteMsP99"` + } `json:"quantiles"` + Ratio struct { + Status5xx float64 `json:"status5xx"` + Status4xx float64 `json:"status4xx"` + } `json:"ratio"` } `json:"httpRequestsAdaptiveGroups"` HTTPRequestsEdgeCountryHost []struct { @@ -484,6 +494,52 @@ func fetchAccounts() []cfaccounts.Account { return cfAccounts } +func fetchZone5mStats(zoneIDs []string) (*cloudflareResponse, error) { + request := graphql.NewRequest(` +query ($zoneIDs: [String!], $mintime: Time!, $maxtime: Time!, $limit: Int!) { + viewer { + zones(filter: { zoneTag_in: $zoneIDs }) { + zoneTag + httpRequestsAdaptiveGroups(filter:{datetime_gt: $mintime, datetime_lt: $maxtime}, limit: $limit) { + dimensions { + originIP + } + ratio { + status4xx + status5xx + } + quantiles { + edgeTimeToFirstByteMsP50 + edgeTimeToFirstByteMsP95 + edgeTimeToFirstByteMsP99 + } + count + } + } + } +`) + + now, _ := GetTimeRange() + request.Var("limit", gqlQueryLimit) + request.Var("maxtime", now) + request.Var("mintime", now.Add(-5*time.Minute)) + request.Var("zoneIDs", zoneIDs) + + gql.Mu.RLock() + defer gql.Mu.RUnlock() + + ctx, cancel := context.WithTimeout(context.Background(), cftimeout) + defer cancel() + + var resp cloudflareResponse + if err := gql.Client.Run(ctx, request, &resp); err != nil { + log.Errorf("failed to fetch zone 5-minute totals, err:%v", err) + return nil, err + } + + return &resp, nil +} + func fetchZoneTotals(zoneIDs []string) (*cloudflareResponse, error) { request := graphql.NewRequest(` query ($zoneIDs: [String!], $mintime: Time!, $maxtime: Time!, $limit: Int!) { diff --git a/main.go b/main.go index c3f1337..067e01d 100644 --- a/main.go +++ b/main.go @@ -128,6 +128,7 @@ func fetchMetrics(deniedMetricsSet MetricsSet) { zoneCount := len(filteredZones) if zoneCount > 0 && zoneCount <= cfgraphqlreqlimit { go fetchZoneAnalytics(filteredZones, &wg, deniedMetricsSet) + go fetchZoneAdaptiveHTTPStats(filteredZones, &wg, deniedMetricsSet) go fetchZoneColocationAnalytics(filteredZones, &wg, deniedMetricsSet) go fetchLoadBalancerAnalytics(filteredZones, &wg, deniedMetricsSet) go fetchLogpushAnalyticsForZone(filteredZones, &wg, deniedMetricsSet) @@ -138,6 +139,7 @@ func fetchMetrics(deniedMetricsSet MetricsSet) { e = zoneCount } go fetchZoneAnalytics(filteredZones[s:e], &wg, deniedMetricsSet) + go fetchZoneAdaptiveHTTPStats(filteredZones[s:e], &wg, deniedMetricsSet) go fetchZoneColocationAnalytics(filteredZones[s:e], &wg, deniedMetricsSet) go fetchLoadBalancerAnalytics(filteredZones[s:e], &wg, deniedMetricsSet) go fetchLogpushAnalyticsForZone(filteredZones[s:e], &wg, deniedMetricsSet) diff --git a/prometheus.go b/prometheus.go index 2673c4c..25a7946 100644 --- a/prometheus.go +++ b/prometheus.go @@ -26,6 +26,8 @@ const ( zoneRequestContentTypeMetricName MetricName = "cloudflare_zone_requests_content_type" zoneRequestCountryMetricName MetricName = "cloudflare_zone_requests_country" zoneRequestHTTPStatusMetricName MetricName = "cloudflare_zone_requests_status" + zoneRequestStatus5mRatioMetricName MetricName = "cloudflare_zone_requests_status_5m_ratio" + zoneRequestEdgeTTFB5mQuantilesMetricName MetricName = "cloudflare_zone_requests_edge_ttfb_5m_quantiles" zoneRequestBrowserMapMetricName MetricName = "cloudflare_zone_requests_browser_map_page_views_count" zoneRequestOriginStatusCountryHostMetricName MetricName = "cloudflare_zone_requests_origin_status_country_host" zoneRequestStatusCountryHostMetricName MetricName = "cloudflare_zone_requests_status_country_host" @@ -112,6 +114,18 @@ var ( }, []string{"zone", "account", "status"}, ) + zoneRequestStatus5mRatio = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: zoneRequestStatus5mRatioMetricName.String(), + Help: "Ratio of 4xx and 5xx responses over the past 5 minutes", + }, []string{"zone", "account", "origin_ip", "status_group"}, + ) + + zoneRequestEdgeTTFB5mQuantiles = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: zoneRequestEdgeTTFB5mQuantilesMetricName.String(), + Help: "Time to first byte (TTFB) quantiles over the past 5 minutes", + }, []string{"zone", "account", "origin_ip", "quantile"}, + ) + zoneRequestBrowserMap = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: zoneRequestBrowserMapMetricName.String(), Help: "Number of successful requests for HTML pages per zone", @@ -330,6 +344,8 @@ func buildAllMetricsSet() MetricsSet { allMetricsSet.Add(zoneRequestContentTypeMetricName) allMetricsSet.Add(zoneRequestCountryMetricName) allMetricsSet.Add(zoneRequestHTTPStatusMetricName) + allMetricsSet.Add(zoneRequestStatus5mRatioMetricName) + allMetricsSet.Add(zoneRequestEdgeTTFB5mQuantilesMetricName) allMetricsSet.Add(zoneRequestBrowserMapMetricName) allMetricsSet.Add(zoneRequestOriginStatusCountryHostMetricName) allMetricsSet.Add(zoneRequestStatusCountryHostMetricName) @@ -399,6 +415,12 @@ func mustRegisterMetrics(deniedMetrics MetricsSet) { if !deniedMetrics.Has(zoneRequestHTTPStatusMetricName) { prometheus.MustRegister(zoneRequestHTTPStatus) } + if !deniedMetrics.Has(zoneRequestStatus5mRatioMetricName) { + prometheus.MustRegister(zoneRequestStatus5mRatio) + } + if !deniedMetrics.Has(zoneRequestEdgeTTFB5mQuantilesMetricName) { + prometheus.MustRegister(zoneRequestEdgeTTFB5mQuantiles) + } if !deniedMetrics.Has(zoneRequestBrowserMapMetricName) { prometheus.MustRegister(zoneRequestBrowserMap) } @@ -706,6 +728,75 @@ func fetchZoneColocationAnalytics(zones []cfzones.Zone, wg *sync.WaitGroup, deni } } +func fetchZoneAdaptiveHTTPStats(zones []cfzones.Zone, wg *sync.WaitGroup, deniedMetricsSet MetricsSet) { + wg.Add(1) + defer wg.Done() + + if deniedMetricsSet.Has(zoneRequestStatus5mRatioMetricName) && deniedMetricsSet.Has(zoneRequestEdgeTTFB5mQuantilesMetricName) { + return + } + + zoneIDs := extractZoneIDs(zones) + if len(zoneIDs) == 0 { + return + } + + r, err := fetchZone5mStats(zoneIDs) + if err != nil { + log.Error("failed to fetch analyzed zone 5-minute stats: ", err) + return + } + + for _, z := range r.Viewer.Zones { + name, account := findZoneAccountName(zones, z.ZoneTag) + + if !deniedMetricsSet.Has(zoneRequestStatus5mRatioMetricName) { + for _, g := range z.HTTPRequestsAdaptiveGroups { + zoneRequestStatus5mRatio.With( + prometheus.Labels{ + "zone": name, + "account": account, + "origin_ip": g.Dimensions.OriginIP, + "status_group": "4xx", + }).Add(float64(g.Ratio.Status4xx)) + zoneRequestStatus5mRatio.With( + prometheus.Labels{ + "zone": name, + "account": account, + "origin_ip": g.Dimensions.OriginIP, + "status_group": "5xx", + }).Add(float64(g.Ratio.Status5xx)) + } + } + + if !deniedMetricsSet.Has(zoneRequestEdgeTTFB5mQuantilesMetricName) { + for _, g := range z.HTTPRequestsAdaptiveGroups { + zoneRequestEdgeTTFB5mQuantiles.With( + prometheus.Labels{ + "zone": name, + "account": account, + "origin_ip": g.Dimensions.OriginIP, + "quantile": "P50", + }).Set(float64(g.Quantiles.EdgeTimeToFirstByteMsP50)) + zoneRequestEdgeTTFB5mQuantiles.With( + prometheus.Labels{ + "zone": name, + "account": account, + "origin_ip": g.Dimensions.OriginIP, + "quantile": "P95", + }).Set(float64(g.Quantiles.EdgeTimeToFirstByteMsP95)) + zoneRequestEdgeTTFB5mQuantiles.With( + prometheus.Labels{ + "zone": name, + "account": account, + "origin_ip": g.Dimensions.OriginIP, + "quantile": "P99", + }).Set(float64(g.Quantiles.EdgeTimeToFirstByteMsP99)) + } + } + } +} + func fetchZoneAnalytics(zones []cfzones.Zone, wg *sync.WaitGroup, deniedMetricsSet MetricsSet) { wg.Add(1) defer wg.Done()