Skip to content

Instantly share code, notes, and snippets.

@maesoser
Created July 24, 2025 18:05
Show Gist options
  • Save maesoser/5797f73d00b340b783d0d801b4f976ff to your computer and use it in GitHub Desktop.
Save maesoser/5797f73d00b340b783d0d801b4f976ff to your computer and use it in GitHub Desktop.
This Cloudflare Worker exposes an endpoint that fetches Cloudflare health check data via GraphQL, transforms it into Prometheus-compatible metrics, and caches the result for a short period.
export default {
async fetch(request, env, ctx) {
// Only allow GET requests
if (request.method !== 'GET') {
return new Response('Method not allowed', { status: 405 })
}
const url = new URL(request.url)
if (url.pathname === '/status') {
return new Response('OK', { status: 200 })
}else if (url.pathname != '/metrics'){
return new Response('Not found', { status: 404 })
}
try {
// Get Cloudflare API credentials from environment variables
const ZONE_TAG = env.CLOUDFLARE_ZONE_TAG || '';
const API_TOKEN = env.CLOUDFLARE_API_TOKEN || '';
const GRAPHQL_ENDPOINT = 'https://api.cloudflare.com/client/v4/graphql';
if (!API_TOKEN || !ZONE_TAG) {
return new Response('Missing Cloudflare env variables', { status: 500 })
}
const now = new Date()
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000)
const cacheKey = new Request(`${url.toString()}/${ZONE_TAG}`, request);
const cache = caches.default;
let response = await cache.match(cacheKey);
if (!response) {
const checks = await fetchGraphQLData(GRAPHQL_ENDPOINT, API_TOKEN, ZONE_TAG, fiveMinutesAgo.toISOString(), now.toISOString())
const metrics = convertToPrometheusMetrics(checks)
response = new Response(metrics, {
headers: {
'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
'Cache-Control': 's-maxage=150'
}
})
const responseToCache = response.clone();
ctx.waitUntil(cache.put(cacheKey, responseToCache));
}
return response;
} catch (error) {
console.error('Error:', error)
return new Response(`Internal server error: ${error.message}`, { status: 500 })
}
}
}
async function fetchGraphQLData(GRAPHQL_ENDPOINT, API_TOKEN, ZONE_TAG, since, until){
// GraphQL query
const query = `
query($zone: String!) {
viewer {
zones(filter: { zoneTag: $zone }) {
checks: healthCheckEventsAdaptive(
limit: 10000,
filter: { datetime_geq: $since, datetime_leq: $until }
) {
rttMs
tcpConnMs
timeToFirstByteMs
tlsHandshakeMs
fqdn
failureReason
healthStatus
originResponseStatus
region
}
}
}
}
`
const variables = {
zone: ZONE_TAG,
since: since,
until: until,
}
// Make GraphQL request to Cloudflare Analytics API
const graphqlResponse = await fetch(GRAPHQL_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_TOKEN}`,
},
body: JSON.stringify({ query, variables: variables })
})
if (!graphqlResponse.ok) {
throw new Error(`GraphQL request failed: ${graphqlResponse.status}`)
}
const data = await graphqlResponse.json()
// Check for GraphQL errors
if (data.errors) {
console.error('GraphQL errors:', data.errors)
return new Response(`GraphQL errors: ${JSON.stringify(data.errors)}`, { status: 500 })
}
// Extract health check data
const checks = data.data?.viewer?.zones?.[0]?.checks || []
return checks
}
function convertToPrometheusMetrics(checks) {
const metrics = []
// Add metadata
metrics.push('# HELP cloudflare_health_check_rtt_ms Round trip time in milliseconds')
metrics.push('# TYPE cloudflare_health_check_rtt_ms gauge')
metrics.push('# HELP cloudflare_health_check_tcp_conn_ms TCP connection time in milliseconds')
metrics.push('# TYPE cloudflare_health_check_tcp_conn_ms gauge')
metrics.push('# HELP cloudflare_health_check_ttfb_ms Time to first byte in milliseconds')
metrics.push('# TYPE cloudflare_health_check_ttfb_ms gauge')
metrics.push('# HELP cloudflare_health_check_tls_handshake_ms TLS handshake time in milliseconds')
metrics.push('# TYPE cloudflare_health_check_tls_handshake_ms gauge')
metrics.push('# HELP cloudflare_health_check_status Health check status (1 = healthy, 0 = unhealthy)')
metrics.push('# TYPE cloudflare_health_check_status gauge')
metrics.push('# HELP cloudflare_health_check_response_status HTTP response status code')
metrics.push('# TYPE cloudflare_health_check_response_status gauge')
// Process each health check
checks.forEach(check => {
const labels = `fqdn="${check.fqdn}",region="${check.region}",failure_reason="${check.failureReason}"`
// RTT metrics
if (check.rttMs !== null && check.rttMs !== undefined) {
metrics.push(`cloudflare_health_check_rtt_ms{${labels}} ${check.rttMs}`)
}
// TCP connection time
if (check.tcpConnMs !== null && check.tcpConnMs !== undefined) {
metrics.push(`cloudflare_health_check_tcp_conn_ms{${labels}} ${check.tcpConnMs}`)
}
// Time to first byte
if (check.timeToFirstByteMs !== null && check.timeToFirstByteMs !== undefined) {
metrics.push(`cloudflare_health_check_ttfb_ms{${labels}} ${check.timeToFirstByteMs}`)
}
// TLS handshake time
if (check.tlsHandshakeMs !== null && check.tlsHandshakeMs !== undefined) {
metrics.push(`cloudflare_health_check_tls_handshake_ms{${labels}} ${check.tlsHandshakeMs}`)
}
// Health status (convert to numeric)
const healthStatus = check.healthStatus === 'healthy' ? 1 : 0
metrics.push(`cloudflare_health_check_status{${labels}} ${healthStatus}`)
// Response status
if (check.originResponseStatus !== null && check.originResponseStatus !== undefined) {
metrics.push(`cloudflare_health_check_response_status{${labels}} ${check.originResponseStatus}`)
}
})
// Add a timestamp
const timestamp = Date.now()
metrics.push(`# EOF`)
return metrics.join('\n') + '\n'
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment