Skip to content

Commit

Permalink
add kv stats
Browse files Browse the repository at this point in the history
Signed-off-by: rare-magma <[email protected]>
  • Loading branch information
rare-magma committed Sep 27, 2024
1 parent 1c93347 commit e6238b5
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 3 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ install:
&& chmod +x $${HOME}/.local/bin/cloudflare_exporter.sh \
&& cp --no-clobber cloudflare_exporter.conf $${HOME}/.config/cloudflare_exporter.conf \
&& cp --no-clobber cloudflare_zone_list.json $${HOME}/.config/cloudflare_zone_list.json \
&& cp --no-clobber cloudflare_kv_namespaces_list.conf $${HOME}/.config/cloudflare_kv_namespaces_list.conf \
&& chmod 400 $${HOME}/.config/cloudflare_exporter.conf \
&& cp cloudflare-exporter.timer $${HOME}/.config/systemd/user/ \
&& cp cloudflare-exporter.service $${HOME}/.config/systemd/user/ \
Expand All @@ -16,6 +17,7 @@ uninstall:
@rm -f $${HOME}/.local/bin/cloudflare_exporter.sh \
&& rm -f $${HOME}/.config/cloudflare_exporter.conf \
&& rm -f $${HOME}/.config/cloudflare_zone_list.json \
&& rm -f $${HOME}/.config/cloudflare_kv_namespaces_list.conf \
&& systemctl --user disable --now cloudflare-exporter.timer \
&& rm -f $${HOME}/.config/.config/systemd/user/cloudflare-exporter.timer \
&& rm -f $${HOME}/.config/systemd/user/cloudflare-exporter.service
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ Bash script that uploads the Cloudflare Analytics API data to influxdb on an hou
docker build . --tag cloudflare-exporter
```

1. Configure `cloudflare_exporter.conf` and `cloudflare_zone_list.json` (see the configuration section below).
1. Configure `cloudflare_exporter.conf`, `cloudflare_zone_list.json` and `cloudflare_kv_namespaces.conf` (see the configuration section below).
1. Run it.

```bash
docker run --rm --init --tty --interactive --read-only --cap-drop ALL --security-opt no-new-privileges:true --cpus 2 -m 64m --pids-limit 16 --volume ./cloudflare_exporter.conf:/app/cloudflare_exporter.conf:ro --volume ./cloudflare_zone_list.json:/app/cloudflare_zone_list.json:ro ghcr.io/rare-magma/cloudflare-exporter:latest
docker run --rm --init --tty --interactive --read-only --cap-drop ALL --security-opt no-new-privileges:true --cpus 2 -m 64m --pids-limit 16 --volume ./cloudflare_exporter.conf:/app/cloudflare_exporter.conf:ro --volume ./cloudflare_zone_list.json:/app/cloudflare_zone_list.json:ro --volume ./cloudflare_kv_namespaces_list.conf:/app/cloudflare_kv_namespaces_list.conf:ro ghcr.io/rare-magma/cloudflare-exporter:latest
```

### With the Makefile
Expand All @@ -61,13 +61,14 @@ For convenience, you can install this exporter with the following command or fol
make install
$EDITOR $HOME/.config/cloudflare_exporter.conf
$EDITOR $HOME/.config/cloudflare_zone_list.json
$EDITOR $HOME/.config/cloudflare_kv_namespaces_list.conf
```

### Manually

1. Copy `cloudflare_exporter.sh` to `$HOME/.local/bin/` and make it executable.

2. Copy `cloudflare_exporter.conf` and `cloudflare_zone_list.json` to `$HOME/.config/`, configure them (see the configuration section below) and make them read only.
2. Copy `cloudflare_exporter.conf`, `cloudflare_zone_list.json` and `cloudflare_kv_namespaces_list.conf` to `$HOME/.config/`, configure them (see the configuration section below) and make them read only.

3. Copy the systemd unit and timer to `$HOME/.config/systemd/user/`:

Expand Down Expand Up @@ -124,6 +125,15 @@ The zone list file should contain a list of zone ids and domain names in json fo
]
```
### KV namespaces list file
The KV namespaces list file is optional and should contain a KV namespace id per line:
```plaintext
999999aba99dd9999ef99ab78965ab1c
111111aba11dd1111ef11ab11111ab1c
```
## Troubleshooting
Run the script manually with bash set to trace:
Expand All @@ -148,6 +158,8 @@ systemctl --user list-timers
- cloudflare_stats_responses: Request statistics broken down by response status code
- cloudflare_stats: General request statistics
- cloudflare_stats_workers: Workers statistics grouped by hour
- cloudflare_stats_kv_ops: KV operation statistics grouped by hour
- cloudflare_stats_kv_storage: KV storage statistics
## Exported metrics example
Expand All @@ -159,6 +171,8 @@ cloudflare_stats_ip,zone="example.com",ipType="searchEngine" requests=21 1703894
cloudflare_stats_responses,zone="example.com",status=403 requests=1 1703894400
cloudflare_stats,zone="example.com" bytes=2032039,cachedBytes=40607,cachedRequests=17,encryptedBytes=2020727,encryptedRequests=251,pageViews=178,requests=266,threats=0,uniqueVisitors=2 1703894400'
cloudflare_stats_workers,account=aa0a0aa000a0000aa00a00aa0e000a0a,worker=worker-name status="scriptThrewException",cpuTimeP50=1246,cpuTimeP99=1246,durationP50=0.001246,durationP99=0.001246,responseBodySizeP50=0,responseBodySizeP99=0,wallTimeP50=1605,wallTimeP99=1605,clientDisconnects=0,cpuTimeUs=1246,duration=0.001246,errors=1,requests=1,responseBodySize=0,subrequests=0,wallTime=1605 1727340566
cloudflare_stats_kv_ops,account=aa0a0aa000a0000aa00a00aa0e000a0a,namespace=999999aba99dd9999ef99ab78965ab1c actionType="read",result="hot_read",responseStatusCode=200,latencyMsP50=116,latencyMsP99=116,objectBytes=1737,requests=1 1727445600
cloudflare_stats_kv_storage,account=aa0a0aa000a0000aa00a00aa0e000a0a,namespace=999999aba99dd9999ef99ab78965ab1c byteCount=5369,keyCount=1 1727442000
```

## Example grafana dashboard
Expand Down Expand Up @@ -197,6 +211,7 @@ Delete the following files:
~/.local/bin/cloudflare_exporter.sh
~/.config/cloudflare_exporter.conf
~/.config/cloudflare_zone_list.json
~/.config/cloudflare_kv_namespaces_list.conf
~/.config/systemd/user/cloudflare-exporter.timer
~/.config/systemd/user/cloudflare-exporter.service
```
Expand Down
1 change: 1 addition & 0 deletions cloudflare-exporter.service
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ After=network-online.target
ExecStart=/home/%u/.local/bin/cloudflare_exporter.sh
LoadCredential=creds:/home/%u/.config/cloudflare_exporter.conf
LoadCredential=list:/home/%u/.config/cloudflare_zone_list.json
LoadCredential=namespaces_list:/home/%u/.config/cloudflare_kv_namespaces_list.conf
Type=oneshot

# Security hardening options
Expand Down
170 changes: 170 additions & 0 deletions cloudflare_exporter.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ JQ=$(command -v jq)
if [[ "${RUNNING_IN_DOCKER}" ]]; then
source "/app/cloudflare_exporter.conf"
CLOUDFLARE_ZONE_LIST=$($CAT /app/cloudflare_zone_list.json)
CLOUDFLARE_KV_NAMESPACES=$($CAT /app/cloudflare_kv_namespaces_list.conf)
elif [[ -f $CREDENTIALS_DIRECTORY/creds ]]; then
#shellcheck source=/dev/null
source "$CREDENTIALS_DIRECTORY/creds"
CLOUDFLARE_ZONE_LIST=$($CAT $CREDENTIALS_DIRECTORY/list)
CLOUDFLARE_KV_NAMESPACES=$($CAT $CREDENTIALS_DIRECTORY/namespaces_list)
else
source "./cloudflare_exporter.conf"
CLOUDFLARE_ZONE_LIST=$($CAT ./cloudflare_zone_list.json)
CLOUDFLARE_KV_NAMESPACES=$($CAT ./cloudflare_kv_namespaces_list.conf)
fi

[[ -z "${INFLUXDB_HOST}" ]] && echo >&2 "INFLUXDB_HOST is empty. Aborting" && exit 1
Expand All @@ -42,6 +45,7 @@ fi
RFC_CURRENT_DATE=$($DATE --rfc-3339=date)
ISO_CURRENT_DATE_TIME=$($DATE --iso-8601=seconds)
ISO_CURRENT_DATE_TIME_1H_AGO=$($DATE --iso-8601=seconds --date "1 hour ago")
ISO_CURRENT_DATE_TIME_2H_AGO=$($DATE --iso-8601=seconds --date "2 hour ago")
INFLUXDB_URL="https://$INFLUXDB_HOST/api/v2/write?precision=s&org=$ORG&bucket=$BUCKET"
CF_URL="https://api.cloudflare.com/client/v4/graphql"

Expand Down Expand Up @@ -365,3 +369,169 @@ if [[ $cf_nb_invocations -gt 0 ]]; then
--data-binary @-

fi

if [[ -n "${CLOUDFLARE_KV_NAMESPACES}" ]]; then

for kv_namespace_id in $(echo "${CLOUDFLARE_KV_NAMESPACES}"); do
KV_GRAPHQL_QUERY=$(
cat <<END_HEREDOC
{ "query":
"query {
viewer {
accounts(filter: { accountTag: \$accountTag }) {
kvOperationsAdaptiveGroups(
filter: { namespaceId: \$namespaceId, datetimeHour_geq: \$datetimeStart, datetimeHour_leq: \$datetimeEnd }
limit: 10000
) {
sum {
objectBytes
requests
}
quantiles {
latencyMsP50
latencyMsP99
}
dimensions {
actionType
datetimeHour
namespaceId
responseStatusCode
result
}
}
}
}
}",
"variables": {
"accountTag": "$CLOUDFLARE_ACCOUNT_TAG",
"namespaceId": "$kv_namespace_id",
"datetimeStart": "$ISO_CURRENT_DATE_TIME_1H_AGO",
"datetimeEnd": "$ISO_CURRENT_DATE_TIME"
}
}
END_HEREDOC
)

cf_kv_json=$(
$CURL --silent --fail --show-error --compressed \
--request POST \
--header "Content-Type: application/json" \
--header "$CF_EMAIL_HEADER" \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
--data "$(echo -n $KV_GRAPHQL_QUERY)" \
"$CF_URL"
)

cf_kv_nb_errors=$(echo $cf_kv_json | $JQ ".errors | length")

if [[ $cf_kv_nb_errors -gt 0 ]]; then
cf_kv_errors=$(echo $cf_kv_json | $JQ --raw-output ".errors[] | .message")
printf "Cloudflare API request failed with: \n%s\nAborting\n" "$cf_kv_errors" >&2
exit 1
fi

cf_kv_json_parsed=$(echo $cf_kv_json | $JQ ".data.viewer.accounts[0].kvOperationsAdaptiveGroups")
cf_stats_kv=$(
echo "$cf_kv_json_parsed" |
$JQ --raw-output "
(.[] |
[\"${CLOUDFLARE_ACCOUNT_TAG}\",
.dimensions.namespaceId,
.dimensions.actionType,
.dimensions.result,
.dimensions.responseStatusCode,
.quantiles.latencyMsP50,
.quantiles.latencyMsP99,
.sum.objectBytes,
.sum.requests,
(.dimensions.datetimeHour | fromdateiso8601)
])
| @tsv" |
$AWK '{printf "cloudflare_stats_kv_ops,account=%s,namespace=%s actionType=\"%s\",result=\"%s\",responseStatusCode=%s,latencyMsP50=%s,latencyMsP99=%s,objectBytes=%s,requests=%s %s\n", $1, $2, $3, $4, $5, $6, $7, $8, $9, $10}'
)

echo "$cf_stats_kv" | $GZIP |
$CURL --silent --fail --show-error \
--request POST "${INFLUXDB_URL}" \
--header 'Content-Encoding: gzip' \
--header "Authorization: Token $INFLUXDB_API_TOKEN" \
--header "Content-Type: text/plain; charset=utf-8" \
--header "Accept: application/json" \
--data-binary @-

KV_STORAGE_GRAPHQL_QUERY=$(
cat <<END_HEREDOC
{ "query":
"query {
viewer {
accounts(filter: { accountTag: \$accountTag }) {
kvStorageAdaptiveGroups(
filter: { namespaceId: \$namespaceId, datetimeHour_geq: \$datetimeStart, datetimeHour_leq: \$datetimeEnd }
limit: 10000
) {
max {
keyCount
byteCount
}
dimensions {
datetimeHour
namespaceId
}
}
}
}
}",
"variables": {
"accountTag": "$CLOUDFLARE_ACCOUNT_TAG",
"namespaceId": "$kv_namespace_id",
"datetimeStart": "$ISO_CURRENT_DATE_TIME_2H_AGO",
"datetimeEnd": "$ISO_CURRENT_DATE_TIME"
}
}
END_HEREDOC
)

cf_kv_storage_json=$(
$CURL --silent --fail --show-error --compressed \
--request POST \
--header "Content-Type: application/json" \
--header "$CF_EMAIL_HEADER" \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
--data "$(echo -n $KV_STORAGE_GRAPHQL_QUERY)" \
"$CF_URL"
)

cf_kv_storage_nb_errors=$(echo $cf_kv_storage_json | $JQ ".errors | length")

if [[ $cf_kv_storage_nb_errors -gt 0 ]]; then
cf_kv_storage_errors=$(echo $cf_kv_storage_json | $JQ --raw-output ".errors[] | .message")
printf "Cloudflare API request failed with: \n%s\nAborting\n" "$cf_kv_storage_errors" >&2
exit 1
fi

cf_kv_storage_json_parsed=$(echo $cf_kv_storage_json | $JQ ".data.viewer.accounts[0].kvStorageAdaptiveGroups")
cf_stats_kv_storage=$(
echo "$cf_kv_storage_json_parsed" |
$JQ --raw-output "
(.[] |
[\"${CLOUDFLARE_ACCOUNT_TAG}\",
.dimensions.namespaceId,
.max.byteCount,
.max.keyCount,
(.dimensions.datetimeHour | fromdateiso8601)
])
| @tsv" |
$AWK '{printf "cloudflare_stats_kv_storage,account=%s,namespace=%s byteCount=%s,keyCount=%s %s\n", $1, $2, $3, $4, $5}'
)

echo "$cf_stats_kv_storage" | $GZIP |
$CURL --silent --fail --show-error \
--request POST "${INFLUXDB_URL}" \
--header 'Content-Encoding: gzip' \
--header "Authorization: Token $INFLUXDB_API_TOKEN" \
--header "Content-Type: text/plain; charset=utf-8" \
--header "Accept: application/json" \
--data-binary @-
done

fi
Empty file.
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ services:
volumes:
- ./cloudflare_exporter.conf:/app/cloudflare_exporter.conf:ro
- ./cloudflare_zone_list.json:/app/cloudflare_zone_list.json:ro
- ./cloudflare_kv_namespaces_list.conf:/app/cloudflare_kv_namespaces_list.conf:ro
labels:
net.reddec.scheduler.cron: "5 * * * *"

0 comments on commit e6238b5

Please sign in to comment.