Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions cloudflare.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ package main

import (
"context"
"encoding/json"
"fmt"
"net/http"
"slices"
"strings"

"github.com/spf13/viper"

cf "github.com/cloudflare/cloudflare-go/v4"
cfaccounts "github.com/cloudflare/cloudflare-go/v4/accounts"
cfcustomhostnames "github.com/cloudflare/cloudflare-go/v4/custom_hostnames"
cfload_balancers "github.com/cloudflare/cloudflare-go/v4/load_balancers"
cfpagination "github.com/cloudflare/cloudflare-go/v4/packages/pagination"
cfrulesets "github.com/cloudflare/cloudflare-go/v4/rulesets"
Expand Down Expand Up @@ -982,3 +988,79 @@ func filterNonFreePlanZones(zones []cfzones.Zone) (filteredZones []cfzones.Zone)
}
return
}

type customHostnameQuota struct {
Allocated int `json:"allocated"`
Used int `json:"used"`
}

func fetchCustomHostnamesCount(zoneID string) (int, error) {
ctx, cancel := context.WithTimeout(context.Background(), cftimeout)
defer cancel()

// Use the SDK to fetch custom hostnames count
// We only need the count, so we request 1 item per page
rep, err := cfclient.CustomHostnames.List(ctx, cfcustomhostnames.CustomHostnameListParams{
ZoneID: cf.F(zoneID),
PerPage: cf.F(1.0),
})
if err != nil {
return 0, fmt.Errorf("failed to fetch custom hostnames: %w", err)
}

// Extract total_count from the result_info extra fields
totalCountRaw := rep.ResultInfo.JSON.ExtraFields["total_count"].Raw()

var totalCount int
if err := json.Unmarshal([]byte(totalCountRaw), &totalCount); err != nil {
return 0, fmt.Errorf("failed to parse total_count: %w", err)
}

return totalCount, nil
}

func fetchCustomHostnamesQuota(zoneID string) (*customHostnameQuota, error) {
ctx, cancel := context.WithTimeout(context.Background(), cftimeout)
defer cancel()

// Use direct HTTP call since this is an undocumented endpoint not in the SDK
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/custom_hostnames/quota", zoneID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

// Add authentication header
apiToken := viper.GetString("cf_api_token")
if apiToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiToken))
}
req.Header.Set("Content-Type", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()

var response struct {
Result customHostnameQuota `json:"result"`
Success bool `json:"success"`
Errors []struct {
Message string `json:"message"`
} `json:"errors"`
}

if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

if !response.Success {
if len(response.Errors) > 0 {
return nil, fmt.Errorf("API error: %s", response.Errors[0].Message)
}
return nil, fmt.Errorf("API returned success=false")
}

return &response.Result, nil
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ func fetchMetrics(accounts []cfaccounts.Account, zones []cfzones.Zone, metrics M
wg.Go(func() { fetchZoneColocationAnalytics(metrics, zonesChunk) })
wg.Go(func() { fetchLoadBalancerAnalytics(metrics, zonesChunk) })
wg.Go(func() { fetchLogpushAnalyticsForZone(metrics, zonesChunk) })
wg.Go(func() { fetchCustomHostnamesMetrics(metrics, zonesChunk) })
}

wg.Wait()
Expand Down
58 changes: 58 additions & 0 deletions prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ const (
r2StorageTotalMetricName MetricName = "cloudflare_r2_storage_total_bytes"
r2StorageMetricName MetricName = "cloudflare_r2_storage_bytes"
r2OperationMetricName MetricName = "cloudflare_r2_operation_count"
zoneCustomHostnamesTotalMetricName MetricName = "cloudflare_zone_custom_hostnames_total"
zoneCustomHostnamesQuotaAllocatedMetricName MetricName = "cloudflare_zone_custom_hostnames_quota_allocated"
zoneCustomHostnamesQuotaUsedMetricName MetricName = "cloudflare_zone_custom_hostnames_quota_used"
)

type MetricsMap map[MetricName]prometheus.Collector
Expand Down Expand Up @@ -289,6 +292,21 @@ var (
Help: "Number of operations performed by R2",
}, []string{"account", "bucket", "operation"})

zoneCustomHostnamesTotal = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: zoneCustomHostnamesTotalMetricName.String(),
Help: "Total number of custom hostnames configured for the zone",
}, []string{"zone", "account"})

zoneCustomHostnamesQuotaAllocated = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: zoneCustomHostnamesQuotaAllocatedMetricName.String(),
Help: "Allocated quota for custom hostnames for the zone",
}, []string{"zone", "account"})

zoneCustomHostnamesQuotaUsed = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: zoneCustomHostnamesQuotaUsedMetricName.String(),
Help: "Used custom hostnames quota for the zone",
}, []string{"zone", "account"})

metricsMap = MetricsMap{}
)

Expand Down Expand Up @@ -330,6 +348,9 @@ func init() {
metricsMap[r2StorageTotalMetricName] = r2StorageTotal
metricsMap[r2StorageMetricName] = r2Storage
metricsMap[r2OperationMetricName] = r2Operation
metricsMap[zoneCustomHostnamesTotalMetricName] = zoneCustomHostnamesTotal
metricsMap[zoneCustomHostnamesQuotaAllocatedMetricName] = zoneCustomHostnamesQuotaAllocated
metricsMap[zoneCustomHostnamesQuotaUsedMetricName] = zoneCustomHostnamesQuotaUsed
}

func buildDeniedMetricsSet(metricsDenylist []string) (MetricsMap, error) {
Expand Down Expand Up @@ -840,3 +861,40 @@ func addLoadBalancingRequestsAdaptive(z *lbResp, name string, account string) {
}
}
}

func fetchCustomHostnamesMetrics(metrics MetricsMap, zones []cfzones.Zone) {
skipTotal := shouldSkip(metrics, zoneCustomHostnamesTotalMetricName)
skipQuota := shouldSkip(metrics, zoneCustomHostnamesQuotaAllocatedMetricName, zoneCustomHostnamesQuotaUsedMetricName)

if skipTotal && skipQuota {
return
}

for _, zone := range zones {
labels := prometheus.Labels{
"zone": zone.Name,
"account": zone.Account.Name,
}

// Fetch count if needed
if !skipTotal {
count, err := fetchCustomHostnamesCount(zone.ID)
if err != nil {
log.Errorf("failed to fetch custom hostnames count for zone %s: %v", zone.Name, err)
} else {
zoneCustomHostnamesTotal.With(labels).Set(float64(count))
}
}

// Fetch quota if needed
if !skipQuota {
quota, err := fetchCustomHostnamesQuota(zone.ID)
if err != nil {
log.Errorf("failed to fetch custom hostnames quota for zone %s: %v", zone.Name, err)
} else {
zoneCustomHostnamesQuotaAllocated.With(labels).Set(float64(quota.Allocated))
zoneCustomHostnamesQuotaUsed.With(labels).Set(float64(quota.Used))
}
}
}
}
Loading