Skip to content

Commit 30a3e17

Browse files
committed
SD-11577: add support for kv metrics
1 parent 1e8dfad commit 30a3e17

File tree

4 files changed

+157
-0
lines changed

4 files changed

+157
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Required authentication scopes:
3535
- `Zone/Firewall Services:Read` is required to fetch zone rule name for `cloudflare_zone_firewall_events_count` metric
3636
- `Account/Account Rulesets:Read` is required to fetch account rule name for `cloudflare_zone_firewall_events_count` metric
3737
- `Account:Load Balancing: Monitors and Pools:Read` is required to fetch pools origin health status `cloudflare_pool_origin_health_status` metric
38+
- `Account/Workers KV Storage:Read` is required for KV metrics
3839
- `Cloudflare Tunnel Read` is required to fetch Cloudflare Tunnel (Cloudflare Zero Trust) metrics
3940

4041
To authenticate this way, only set `CF_API_TOKEN` (omit `CF_API_EMAIL` and `CF_API_KEY`)
@@ -127,6 +128,8 @@ Note: `ZONE_<name>` configuration is not supported as flag.
127128
# HELP cloudflare_r2_operation_count Number of operations performed by R2
128129
# HELP cloudflare_r2_storage_bytes Storage used by R2
129130
# HELP cloudflare_r2_storage_total_bytes Total storage used by R2
131+
# HELP cloudflare_kv_requests_count Number of KV operations by namespace and action type
132+
# HELP cloudflare_kv_latency KV operation latency quantiles (milliseconds)
130133
```
131134

132135
## Helm chart repository

cloudflare.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
cf "github.com/cloudflare/cloudflare-go/v4"
99
cfaccounts "github.com/cloudflare/cloudflare-go/v4/accounts"
10+
cfkv "github.com/cloudflare/cloudflare-go/v4/kv"
1011
cfload_balancers "github.com/cloudflare/cloudflare-go/v4/load_balancers"
1112
cfpagination "github.com/cloudflare/cloudflare-go/v4/packages/pagination"
1213
cfrulesets "github.com/cloudflare/cloudflare-go/v4/rulesets"
@@ -299,6 +300,30 @@ type lbResp struct {
299300
ZoneTag string `json:"zoneTag"`
300301
}
301302

303+
type cloudflareResponseKV struct {
304+
Viewer struct {
305+
Accounts []kvAccountResp `json:"accounts"`
306+
} `json:"viewer"`
307+
}
308+
309+
type kvAccountResp struct {
310+
KvOperationsAdaptiveGroups []struct {
311+
Dimensions struct {
312+
NamespaceID string `json:"namespaceId"`
313+
ActionType string `json:"actionType"`
314+
} `json:"dimensions"`
315+
Sum struct {
316+
Requests uint64 `json:"requests"`
317+
} `json:"sum"`
318+
Quantiles struct {
319+
LatencyMsP50 float32 `json:"latencyMsP50"`
320+
LatencyMsP75 float32 `json:"latencyMsP75"`
321+
LatencyMsP99 float32 `json:"latencyMsP99"`
322+
LatencyMsP999 float32 `json:"latencyMsP999"`
323+
} `json:"quantiles"`
324+
} `json:"kvOperationsAdaptiveGroups"`
325+
}
326+
302327
type cloudflareResponseDNSFirewall struct {
303328
Viewer struct {
304329
Accounts []dnsFirewallAccountResp `json:"accounts"`
@@ -1085,6 +1110,80 @@ func filterNonFreePlanZones(zones []cfzones.Zone) (filteredZones []cfzones.Zone)
10851110
return
10861111
}
10871112

1113+
func fetchKVNamespaces(accountID string) (map[string]string, error) {
1114+
namespaceMap := make(map[string]string)
1115+
ctx, cancel := context.WithTimeout(context.Background(), cftimeout)
1116+
defer cancel()
1117+
page := cfclient.KV.Namespaces.ListAutoPaging(ctx, cfkv.NamespaceListParams{
1118+
AccountID: cf.F(accountID),
1119+
})
1120+
if page.Err() != nil {
1121+
return nil, page.Err()
1122+
}
1123+
1124+
seenIDs := make(map[string]struct{})
1125+
for page.Next() {
1126+
if page.Err() != nil {
1127+
log.Errorf("error during paging KV namespaces: %v", page.Err())
1128+
break
1129+
}
1130+
ns := page.Current()
1131+
if _, exists := seenIDs[ns.ID]; exists {
1132+
log.Errorf("fetchKVNamespaces: duplicate namespace ID detected (%s), breaking loop", ns.ID)
1133+
break
1134+
}
1135+
seenIDs[ns.ID] = struct{}{}
1136+
namespaceMap[ns.ID] = ns.Title
1137+
}
1138+
1139+
return namespaceMap, nil
1140+
}
1141+
1142+
func fetchKVOperations(accountID string) (*cloudflareResponseKV, error) {
1143+
request := graphql.NewRequest(`
1144+
query ($accountID: String!, $mintime: Time!, $maxtime: Time!, $limit: Int!) {
1145+
viewer {
1146+
accounts(filter: {accountTag: $accountID}) {
1147+
kvOperationsAdaptiveGroups(limit: $limit, filter: {datetime_geq: $mintime, datetime_lt: $maxtime}) {
1148+
dimensions {
1149+
namespaceId
1150+
actionType
1151+
}
1152+
sum {
1153+
requests
1154+
}
1155+
quantiles {
1156+
latencyMsP50
1157+
latencyMsP75
1158+
latencyMsP99
1159+
latencyMsP999
1160+
}
1161+
}
1162+
}
1163+
}
1164+
}`)
1165+
1166+
now, now1mAgo := GetTimeRange()
1167+
request.Var("limit", gqlQueryLimit)
1168+
request.Var("maxtime", now)
1169+
request.Var("mintime", now1mAgo)
1170+
request.Var("accountID", accountID)
1171+
1172+
gql.Mu.RLock()
1173+
defer gql.Mu.RUnlock()
1174+
1175+
ctx, cancel := context.WithTimeout(context.Background(), cftimeout)
1176+
defer cancel()
1177+
1178+
var resp cloudflareResponseKV
1179+
if err := gql.Client.Run(ctx, request, &resp); err != nil {
1180+
log.Errorf("error fetching KV operations, err:%v", err)
1181+
return nil, err
1182+
}
1183+
1184+
return &resp, nil
1185+
}
1186+
10881187
func fetchDNSFirewallTotals(accountID string) (*cloudflareResponseDNSFirewall, error) {
10891188
request := graphql.NewRequest(`
10901189
query ($accountID: string, $mintime: Time!, $maxtime: Time!, $limit: Int!) {

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ func fetchMetrics(deniedMetricsSet MetricsSet) {
110110

111111
for _, a := range accounts {
112112
go fetchWorkerAnalytics(a, &wg)
113+
go fetchKVAnalytics(a, &wg)
113114
go fetchLogpushAnalyticsForAccount(a, &wg)
114115
go fetchR2StorageForAccount(a, &wg)
115116
go fetchLoadblancerPoolsHealth(a, &wg)

prometheus.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ const (
6464
tunnelConnectorInfoMetricName MetricName = "cloudflare_tunnel_connector_info"
6565
tunnelConnectorActiveConnectionsMetricName MetricName = "cloudflare_tunnel_connector_active_connections"
6666
dnsFirewallQueryCountMetricName MetricName = "cloudflare_dns_firewall_query_count"
67+
kvRequestsMetricName MetricName = "cloudflare_kv_requests_count"
68+
kvLatencyMetricName MetricName = "cloudflare_kv_latency"
6769
)
6870

6971
type MetricsSet map[MetricName]struct{}
@@ -336,6 +338,16 @@ var (
336338
Help: "Reports number of active connections for a Cloudflare Tunnel connector",
337339
}, []string{"account", "tunnel_id", "client_id"})
338340

341+
kvRequests = prometheus.NewGaugeVec(prometheus.GaugeOpts{
342+
Name: kvRequestsMetricName.String(),
343+
Help: "Number of KV operations by namespace and action type",
344+
}, []string{"namespace_name", "action_type", "account"})
345+
346+
kvLatency = prometheus.NewGaugeVec(prometheus.GaugeOpts{
347+
Name: kvLatencyMetricName.String(),
348+
Help: "KV operation latency quantiles (milliseconds)",
349+
}, []string{"namespace_name", "action_type", "account", "quantile"})
350+
339351
dnsFirewallQueryCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{
340352
Name: dnsFirewallQueryCountMetricName.String(),
341353
Help: "DNS Firewall query count by query type and response code",
@@ -388,6 +400,8 @@ func buildAllMetricsSet() MetricsSet {
388400
allMetricsSet.Add(tunnelConnectorInfoMetricName)
389401
allMetricsSet.Add(tunnelConnectorActiveConnectionsMetricName)
390402
allMetricsSet.Add(dnsFirewallQueryCountMetricName)
403+
allMetricsSet.Add(kvRequestsMetricName)
404+
allMetricsSet.Add(kvLatencyMetricName)
391405
return allMetricsSet
392406
}
393407

@@ -537,6 +551,12 @@ func mustRegisterMetrics(deniedMetrics MetricsSet) {
537551
if !deniedMetrics.Has(dnsFirewallQueryCountMetricName) {
538552
prometheus.MustRegister(dnsFirewallQueryCount)
539553
}
554+
if !deniedMetrics.Has(kvRequestsMetricName) {
555+
prometheus.MustRegister(kvRequests)
556+
}
557+
if !deniedMetrics.Has(kvLatencyMetricName) {
558+
prometheus.MustRegister(kvLatency)
559+
}
540560
}
541561

542562
func fetchLoadblancerPoolsHealth(account cfaccounts.Account, wg *sync.WaitGroup) {
@@ -607,6 +627,40 @@ func fetchWorkerAnalytics(account cfaccounts.Account, wg *sync.WaitGroup) {
607627
}
608628
}
609629

630+
func fetchKVAnalytics(account cfaccounts.Account, wg *sync.WaitGroup) {
631+
wg.Add(1)
632+
defer wg.Done()
633+
634+
namespaceMap, err := fetchKVNamespaces(account.ID)
635+
if err != nil {
636+
log.Error("failed to fetch KV namespaces for account ", account.ID, ": ", err)
637+
return
638+
}
639+
640+
r, err := fetchKVOperations(account.ID)
641+
if err != nil {
642+
log.Error("failed to fetch KV operations for account ", account.ID, ": ", err)
643+
return
644+
}
645+
646+
accountName := strings.ToLower(strings.ReplaceAll(account.Name, " ", "-"))
647+
648+
for _, a := range r.Viewer.Accounts {
649+
for _, kv := range a.KvOperationsAdaptiveGroups {
650+
namespaceName := namespaceMap[kv.Dimensions.NamespaceID]
651+
if namespaceName == "" {
652+
namespaceName = kv.Dimensions.NamespaceID
653+
}
654+
655+
kvRequests.With(prometheus.Labels{"namespace_name": namespaceName, "action_type": kv.Dimensions.ActionType, "account": accountName}).Set(float64(kv.Sum.Requests))
656+
kvLatency.With(prometheus.Labels{"namespace_name": namespaceName, "action_type": kv.Dimensions.ActionType, "account": accountName, "quantile": "P50"}).Set(float64(kv.Quantiles.LatencyMsP50))
657+
kvLatency.With(prometheus.Labels{"namespace_name": namespaceName, "action_type": kv.Dimensions.ActionType, "account": accountName, "quantile": "P75"}).Set(float64(kv.Quantiles.LatencyMsP75))
658+
kvLatency.With(prometheus.Labels{"namespace_name": namespaceName, "action_type": kv.Dimensions.ActionType, "account": accountName, "quantile": "P99"}).Set(float64(kv.Quantiles.LatencyMsP99))
659+
kvLatency.With(prometheus.Labels{"namespace_name": namespaceName, "action_type": kv.Dimensions.ActionType, "account": accountName, "quantile": "P999"}).Set(float64(kv.Quantiles.LatencyMsP999))
660+
}
661+
}
662+
}
663+
610664
func fetchLogpushAnalyticsForAccount(account cfaccounts.Account, wg *sync.WaitGroup) {
611665
wg.Add(1)
612666
defer wg.Done()

0 commit comments

Comments
 (0)