Skip to content

Instantly share code, notes, and snippets.

@maesoser
Last active July 1, 2025 08:59
Show Gist options
  • Save maesoser/b836fa5d63b7348462278f58f370bd8c to your computer and use it in GitHub Desktop.
Save maesoser/b836fa5d63b7348462278f58f370bd8c to your computer and use it in GitHub Desktop.
Performance exporter worker
export default {
async fetch(request, env, ctx) {
const ACCOUNT_TAG = env.CLOUDFLARE_ACCOUNT_TAG || '';
const API_TOKEN = env.CLOUDFLARE_API_TOKEN || '';
// Only handle GET requests to the metrics endpoint
if (request.method !== 'GET') {
return new Response('Method not allowed', { status: 405 })
}
try {
const metrics = await fetchAndConvertMetrics(ACCOUNT_TAG, API_TOKEN)
return new Response(metrics, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
})
} catch (error) {
console.error('Error fetching metrics:', error)
return new Response(`Error: ${error.message}`, {
status: 500,
headers: { 'Content-Type': 'text/plain' }
})
}
},
};
async function fetchAndConvertMetrics(accountTag, apiToken) {
// Calculate time range for last 5 minutes
const now = new Date()
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000)
// GraphQL query and variables
const query = `
query($account: String!, $since: Time!, $until: Time!, $filter: [HttpRequestsAdaptiveGroupsFilter!]) {
viewer {
accounts(filter: { accountTag: $account }) {
requests: httpRequestsAdaptiveGroups(
limit: 10000,
filter: { datetime_geq: $since, datetime_leq: $until, AND: $filter }
) {
quantiles {
edgeDnsResponseTimeMsP50
edgeDnsResponseTimeMsP95
edgeDnsResponseTimeMsP99
edgeResponseBytesP50
edgeResponseBytesP95
edgeResponseBytesP99
edgeTimeToFirstByteMsP50
edgeTimeToFirstByteMsP95
edgeTimeToFirstByteMsP99
originResponseDurationMsP50
originResponseDurationMsP95
originResponseDurationMsP99
}
avg {
sampleInterval
}
ratio {
status4xx
status5xx
}
dimensions {
clientCountryName
}
}
}
}
}
`
const variables = {
account: accountTag,
since: fiveMinutesAgo.toISOString(),
until: now.toISOString(),
filter: [
{ "requestSource": "eyeball" },
{ "cacheStatus": "dynamic" }
]
}
// Make GraphQL request
const response = await fetch('https://api.cloudflare.com/client/v4/graphql', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`, // Environment variable
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
variables: variables
})
})
if (!response.ok) {
throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`)
}
const data = await response.json()
if (data.errors) {
throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`)
}
return convertToPrometheusMetrics(data.data)
}
function convertToPrometheusMetrics(data) {
const metrics = []
const timestamp = Date.now()
// Add help and type comments for each metric
const metricDefinitions = [
'# HELP cloudflare_edge_dns_response_time_ms Edge DNS response time in milliseconds',
'# TYPE cloudflare_edge_dns_response_time_ms gauge',
'# HELP cloudflare_edge_response_bytes Edge response size in bytes',
'# TYPE cloudflare_edge_response_bytes gauge',
'# HELP cloudflare_edge_time_to_first_byte_ms Time to first byte in milliseconds',
'# TYPE cloudflare_edge_time_to_first_byte_ms gauge',
'# HELP cloudflare_origin_response_duration_ms Origin response duration in milliseconds',
'# TYPE cloudflare_origin_response_duration_ms gauge',
'# HELP cloudflare_status_ratio HTTP status code ratios',
'# TYPE cloudflare_status_ratio gauge',
'# HELP cloudflare_sample_interval_avg Average sample interval',
'# TYPE cloudflare_sample_interval_avg gauge'
]
metrics.push(...metricDefinitions)
// Process each account's data
if (data.viewer && data.viewer.accounts) {
data.viewer.accounts.forEach(account => {
if (account.requests) {
account.requests.forEach(request => {
const country = request.dimensions?.clientCountryName || 'unknown'
const labels = `{country="${country}"}`
// DNS Response Time metrics
if (request.quantiles) {
const q = request.quantiles
if (q.edgeDnsResponseTimeMsP50 !== null) {
metrics.push(`cloudflare_edge_dns_response_time_ms${labels.replace('}', ',quantile="0.5"}')} ${q.edgeDnsResponseTimeMsP50} ${timestamp}`)
}
if (q.edgeDnsResponseTimeMsP95 !== null) {
metrics.push(`cloudflare_edge_dns_response_time_ms${labels.replace('}', ',quantile="0.95"}')} ${q.edgeDnsResponseTimeMsP95} ${timestamp}`)
}
if (q.edgeDnsResponseTimeMsP99 !== null) {
metrics.push(`cloudflare_edge_dns_response_time_ms${labels.replace('}', ',quantile="0.99"}')} ${q.edgeDnsResponseTimeMsP99} ${timestamp}`)
}
// Edge Response Bytes metrics
if (q.edgeResponseBytesP50 !== null) {
metrics.push(`cloudflare_edge_response_bytes${labels.replace('}', ',quantile="0.5"}')} ${q.edgeResponseBytesP50} ${timestamp}`)
}
if (q.edgeResponseBytesP95 !== null) {
metrics.push(`cloudflare_edge_response_bytes${labels.replace('}', ',quantile="0.95"}')} ${q.edgeResponseBytesP95} ${timestamp}`)
}
if (q.edgeResponseBytesP99 !== null) {
metrics.push(`cloudflare_edge_response_bytes${labels.replace('}', ',quantile="0.99"}')} ${q.edgeResponseBytesP99} ${timestamp}`)
}
// Time to First Byte metrics
if (q.edgeTimeToFirstByteMsP50 !== null) {
metrics.push(`cloudflare_edge_time_to_first_byte_ms${labels.replace('}', ',quantile="0.5"}')} ${q.edgeTimeToFirstByteMsP50} ${timestamp}`)
}
if (q.edgeTimeToFirstByteMsP95 !== null) {
metrics.push(`cloudflare_edge_time_to_first_byte_ms${labels.replace('}', ',quantile="0.95"}')} ${q.edgeTimeToFirstByteMsP95} ${timestamp}`)
}
if (q.edgeTimeToFirstByteMsP99 !== null) {
metrics.push(`cloudflare_edge_time_to_first_byte_ms${labels.replace('}', ',quantile="0.99"}')} ${q.edgeTimeToFirstByteMsP99} ${timestamp}`)
}
// Origin Response Duration metrics
if (q.originResponseDurationMsP50 !== null) {
metrics.push(`cloudflare_origin_response_duration_ms${labels.replace('}', ',quantile="0.5"}')} ${q.originResponseDurationMsP50} ${timestamp}`)
}
if (q.originResponseDurationMsP95 !== null) {
metrics.push(`cloudflare_origin_response_duration_ms${labels.replace('}', ',quantile="0.95"}')} ${q.originResponseDurationMsP95} ${timestamp}`)
}
if (q.originResponseDurationMsP99 !== null) {
metrics.push(`cloudflare_origin_response_duration_ms${labels.replace('}', ',quantile="0.99"}')} ${q.originResponseDurationMsP99} ${timestamp}`)
}
}
// Status ratio metrics
if (request.ratio) {
if (request.ratio.status4xx !== null) {
metrics.push(`cloudflare_status_ratio${labels.replace('}', ',status="4xx"}')} ${request.ratio.status4xx} ${timestamp}`)
}
if (request.ratio.status5xx !== null) {
metrics.push(`cloudflare_status_ratio${labels.replace('}', ',status="5xx"}')} ${request.ratio.status5xx} ${timestamp}`)
}
}
// Sample interval metric
if (request.avg && request.avg.sampleInterval !== null) {
metrics.push(`cloudflare_sample_interval_avg${labels} ${request.avg.sampleInterval} ${timestamp}`)
}
})
}
})
}
return metrics.join('\n') + '\n'
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment