Add Cloudflare COLO data to the access log tracking system to capture datacenter location information. This enables better monitoring of request distribution across Cloudflare's global infrastructure. The COLO field is mapped to blob16 in the blobs map and populated from the Cloudflare request headers.
162 lines
4.9 KiB
TypeScript
162 lines
4.9 KiB
TypeScript
import type { H3Event } from 'h3'
|
|
import { getFlag } from '@/utils/flag'
|
|
import { parseAcceptLanguage } from 'intl-parse-accept-language'
|
|
import { UAParser } from 'ua-parser-js'
|
|
import {
|
|
CLIs,
|
|
Crawlers,
|
|
Emails,
|
|
ExtraDevices,
|
|
Fetchers,
|
|
InApps,
|
|
MediaPlayers,
|
|
Vehicles,
|
|
} from 'ua-parser-js/extensions'
|
|
import { parseURL } from 'ufo'
|
|
|
|
function toBlobNumber(blob: string) {
|
|
return +blob.replace(/\D/g, '')
|
|
}
|
|
|
|
export const blobsMap = {
|
|
blob1: 'slug',
|
|
blob2: 'url',
|
|
blob3: 'ua',
|
|
blob4: 'ip',
|
|
blob5: 'referer',
|
|
blob6: 'country',
|
|
blob7: 'region',
|
|
blob8: 'city',
|
|
blob9: 'timezone',
|
|
blob10: 'language',
|
|
blob11: 'os',
|
|
blob12: 'browser',
|
|
blob13: 'browserType',
|
|
blob14: 'device',
|
|
blob15: 'deviceType',
|
|
blob16: 'COLO',
|
|
} as const
|
|
|
|
export const doublesMap = {
|
|
double1: 'latitude',
|
|
double2: 'longitude',
|
|
} as const
|
|
|
|
export type BlobsMap = typeof blobsMap
|
|
export type BlobsKey = keyof BlobsMap
|
|
|
|
export type DoublesMap = typeof doublesMap
|
|
export type DoublesKey = keyof DoublesMap
|
|
|
|
export type LogsKey = BlobsMap[BlobsKey] | DoublesMap[DoublesKey]
|
|
export type LogsMap = {
|
|
[key in BlobsMap[BlobsKey]]: string | undefined
|
|
} & {
|
|
[key in DoublesMap[DoublesKey]]?: number | undefined
|
|
}
|
|
|
|
export const logsMap = {
|
|
...Object.entries(blobsMap).reduce((acc, [k, v]) => ({ ...acc, [v]: k }), {}),
|
|
...Object.entries(doublesMap).reduce((acc, [k, v]) => ({ ...acc, [v]: k }), {}),
|
|
} as LogsMap
|
|
|
|
function logs2blobs(logs: LogsMap) {
|
|
return (Object.keys(blobsMap) as BlobsKey[])
|
|
.sort((a, b) => toBlobNumber(a) - toBlobNumber(b))
|
|
.map(key => String(logs[blobsMap[key] as LogsKey] || ''))
|
|
}
|
|
|
|
function blobs2logs(blobs: string[]) {
|
|
const logsList = Object.keys(blobsMap)
|
|
|
|
return blobs.reduce((logs, blob, i) => {
|
|
const key = blobsMap[logsList[i] as BlobsKey]
|
|
logs[key] = blob
|
|
return logs
|
|
}, {} as Partial<LogsMap>)
|
|
}
|
|
|
|
function logs2doubles(logs: LogsMap) {
|
|
return (Object.keys(doublesMap) as DoublesKey[])
|
|
.sort((a, b) => toBlobNumber(a) - toBlobNumber(b))
|
|
.map(key => Number(logs[doublesMap[key] as LogsKey] || 0))
|
|
}
|
|
|
|
function doubles2logs(doubles: number[]) {
|
|
const logsList = Object.keys(doublesMap)
|
|
|
|
return doubles.reduce((logs, double, i) => {
|
|
const key = doublesMap[logsList[i] as DoublesKey]
|
|
logs[key] = double
|
|
return logs
|
|
}, {} as Partial<LogsMap>)
|
|
}
|
|
|
|
export function useAccessLog(event: H3Event) {
|
|
const ip = getHeader(event, 'cf-connecting-ip') || getHeader(event, 'x-real-ip') || getRequestIP(event, { xForwardedFor: true })
|
|
|
|
const { host: referer } = parseURL(getHeader(event, 'referer'))
|
|
|
|
const acceptLanguage = getHeader(event, 'accept-language') || ''
|
|
const language = (parseAcceptLanguage(acceptLanguage) || [])[0]
|
|
|
|
const userAgent = getHeader(event, 'user-agent') || ''
|
|
const uaInfo = (new UAParser(userAgent, {
|
|
// eslint-disable-next-line ts/ban-ts-comment
|
|
// @ts-expect-error
|
|
browser: [Crawlers.browser || [], CLIs.browser || [], Emails.browser || [], Fetchers.browser || [], InApps.browser || [], MediaPlayers.browser || [], Vehicles.browser || []].flat(),
|
|
// eslint-disable-next-line ts/ban-ts-comment
|
|
// @ts-expect-error
|
|
device: [ExtraDevices.device || []].flat(),
|
|
})).getResult()
|
|
|
|
const { request: { cf } } = event.context.cloudflare
|
|
const link = event.context.link || {}
|
|
|
|
const isBot = cf?.botManagement?.verifiedBot
|
|
|| ['crawler', 'fetcher'].includes(uaInfo?.browser?.type || '')
|
|
|| ['spider', 'bot'].includes(uaInfo?.browser?.name?.toLowerCase() || '')
|
|
|
|
const { disableBotAccessLog } = useRuntimeConfig(event)
|
|
if (isBot && disableBotAccessLog) {
|
|
console.log('bot access log disabled:', userAgent)
|
|
return Promise.resolve()
|
|
}
|
|
|
|
const regionNames = new Intl.DisplayNames(['en'], { type: 'region' })
|
|
const countryName = regionNames.of(cf?.country || 'WD') // fallback to "Worldwide"
|
|
const accessLogs = {
|
|
url: link.url,
|
|
slug: link.slug,
|
|
ua: userAgent,
|
|
ip,
|
|
referer,
|
|
country: cf?.country,
|
|
region: `${getFlag(cf?.country)} ${[cf?.region, countryName].filter(Boolean).join(',')}`,
|
|
city: `${getFlag(cf?.country)} ${[cf?.city, countryName].filter(Boolean).join(',')}`,
|
|
timezone: cf?.timezone,
|
|
language,
|
|
os: uaInfo?.os?.name,
|
|
browser: uaInfo?.browser?.name,
|
|
browserType: uaInfo?.browser?.type,
|
|
device: uaInfo?.device?.model,
|
|
deviceType: uaInfo?.device?.type,
|
|
COLO: cf?.colo,
|
|
|
|
// For RealTime Globe
|
|
latitude: Number(cf?.latitude || getHeader(event, 'cf-iplatitude') || 0),
|
|
longitude: Number(cf?.longitude || getHeader(event, 'cf-iplongitude') || 0),
|
|
}
|
|
|
|
if (process.env.NODE_ENV === 'production') {
|
|
return hubAnalytics().put({
|
|
indexes: [link.id], // only one index
|
|
blobs: logs2blobs(accessLogs),
|
|
doubles: logs2doubles(accessLogs),
|
|
})
|
|
}
|
|
else {
|
|
console.log('access logs:', accessLogs, logs2blobs(accessLogs), logs2doubles(accessLogs), { ...blobs2logs(logs2blobs(accessLogs)), ...doubles2logs(logs2doubles(accessLogs)) })
|
|
return Promise.resolve()
|
|
}
|
|
}
|