Add Globalping API for Korea-based latency measurement in domain diagnostics
Integrates Globalping API to measure ping and HTTP response times from Korean probes (Seoul, Chuncheon, Daejeon). Results include per-probe ping stats and HTTP timing breakdown (DNS, TCP, TLS, TTFB). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import { connect } from 'cloudflare:sockets';
|
||||
import type { NetworkDiagnosticReport, DnsResolverResult, IspDnsBlockResult, HttpCheckResult, TcpCheckResult } from '../types';
|
||||
import type { NetworkDiagnosticReport, DnsResolverResult, IspDnsBlockResult, HttpCheckResult, TcpCheckResult, GlobalpingProbeResult } from '../types';
|
||||
import { createLogger } from '../utils/logger';
|
||||
|
||||
const logger = createLogger('network-diagnostic');
|
||||
@@ -410,6 +410,170 @@ async function checkTcpPorts(domain: string, ports?: number[]): Promise<TcpCheck
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Globalping — Korea Probe Measurements (Ping + HTTP)
|
||||
// ============================================================================
|
||||
|
||||
const GLOBALPING_API = 'https://api.globalping.io/v1/measurements';
|
||||
const GLOBALPING_POLL_INTERVAL_MS = 2000;
|
||||
const GLOBALPING_MAX_WAIT_MS = 15000;
|
||||
|
||||
interface GlobalpingMeasurementResponse {
|
||||
id: string;
|
||||
status: string;
|
||||
results?: GlobalpingRawResult[];
|
||||
}
|
||||
|
||||
interface GlobalpingRawResult {
|
||||
probe: {
|
||||
city: string;
|
||||
network: string;
|
||||
asn: number;
|
||||
};
|
||||
result: {
|
||||
status: string;
|
||||
stats?: {
|
||||
min: number;
|
||||
avg: number;
|
||||
max: number;
|
||||
loss: number;
|
||||
};
|
||||
statusCode?: number;
|
||||
timings?: {
|
||||
total: number;
|
||||
dns: number;
|
||||
tcp: number;
|
||||
tls: number;
|
||||
firstByte: number;
|
||||
};
|
||||
rawOutput?: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function createGlobalpingMeasurement(
|
||||
type: 'ping' | 'http',
|
||||
target: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const body: Record<string, unknown> = {
|
||||
type,
|
||||
target,
|
||||
locations: [{ country: 'KR' }],
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
if (type === 'ping') {
|
||||
body.measurementOptions = { packets: 3 };
|
||||
} else {
|
||||
body.measurementOptions = {
|
||||
protocol: 'HTTPS',
|
||||
request: { method: 'HEAD' },
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(GLOBALPING_API, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn('Globalping measurement creation failed', {
|
||||
type,
|
||||
status: response.status,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { id: string };
|
||||
return data.id;
|
||||
} catch (error) {
|
||||
logger.error('Globalping API error', error as Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function pollGlobalpingResult(id: string): Promise<GlobalpingRawResult[]> {
|
||||
const deadline = Date.now() + GLOBALPING_MAX_WAIT_MS;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(`${GLOBALPING_API}/${id}`);
|
||||
if (!response.ok) return [];
|
||||
|
||||
const data = (await response.json()) as GlobalpingMeasurementResponse;
|
||||
if (data.status === 'finished') {
|
||||
return data.results || [];
|
||||
}
|
||||
|
||||
// Wait before polling again
|
||||
await new Promise((r) => setTimeout(r, GLOBALPING_POLL_INTERVAL_MS));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function checkKoreaProbes(domain: string): Promise<GlobalpingProbeResult[]> {
|
||||
// Create ping and HTTP measurements in parallel
|
||||
const [pingId, httpId] = await Promise.all([
|
||||
createGlobalpingMeasurement('ping', domain),
|
||||
createGlobalpingMeasurement('http', domain),
|
||||
]);
|
||||
|
||||
if (!pingId && !httpId) return [];
|
||||
|
||||
// Poll both results in parallel
|
||||
const [pingResults, httpResults] = await Promise.all([
|
||||
pingId ? pollGlobalpingResult(pingId) : Promise.resolve([]),
|
||||
httpId ? pollGlobalpingResult(httpId) : Promise.resolve([]),
|
||||
]);
|
||||
|
||||
// Merge ping and HTTP results by probe city+network
|
||||
const probeMap = new Map<string, GlobalpingProbeResult>();
|
||||
|
||||
for (const r of pingResults) {
|
||||
const key = `${r.probe.city}:${r.probe.network}`;
|
||||
probeMap.set(key, {
|
||||
city: r.probe.city,
|
||||
network: r.probe.network,
|
||||
asn: r.probe.asn,
|
||||
ping: r.result.stats ? {
|
||||
min: r.result.stats.min,
|
||||
avg: r.result.stats.avg,
|
||||
max: r.result.stats.max,
|
||||
loss: r.result.stats.loss,
|
||||
} : undefined,
|
||||
error: r.result.status !== 'finished' ? r.result.rawOutput : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
for (const r of httpResults) {
|
||||
const key = `${r.probe.city}:${r.probe.network}`;
|
||||
const existing = probeMap.get(key) || {
|
||||
city: r.probe.city,
|
||||
network: r.probe.network,
|
||||
asn: r.probe.asn,
|
||||
};
|
||||
existing.http = r.result.timings ? {
|
||||
statusCode: r.result.statusCode || 0,
|
||||
totalMs: r.result.timings.total,
|
||||
dnsMs: r.result.timings.dns,
|
||||
tcpMs: r.result.timings.tcp,
|
||||
tlsMs: r.result.timings.tls,
|
||||
firstByteMs: r.result.timings.firstByte,
|
||||
} : undefined;
|
||||
if (!existing.error && r.result.status !== 'finished') {
|
||||
existing.error = r.result.rawOutput;
|
||||
}
|
||||
probeMap.set(key, existing);
|
||||
}
|
||||
|
||||
return [...probeMap.values()];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Summary Generation
|
||||
// ============================================================================
|
||||
@@ -418,7 +582,8 @@ function generateSummary(
|
||||
dns: DnsResolverResult[],
|
||||
ispBlocking: IspDnsBlockResult[],
|
||||
http: HttpCheckResult[],
|
||||
tcp: TcpCheckResult[]
|
||||
tcp: TcpCheckResult[],
|
||||
koreaProbes: GlobalpingProbeResult[]
|
||||
): string[] {
|
||||
const summary: string[] = [];
|
||||
|
||||
@@ -479,6 +644,30 @@ function generateSummary(
|
||||
summary.push(`TCP 연결 성공: ${tcpOk.map((r) => `포트 ${r.port} (${r.responseTimeMs}ms)`).join(', ')}`);
|
||||
}
|
||||
|
||||
// Korea probe results
|
||||
if (koreaProbes.length > 0) {
|
||||
const pingProbes = koreaProbes.filter((p) => p.ping);
|
||||
if (pingProbes.length > 0) {
|
||||
const avgPing = pingProbes.reduce((sum, p) => sum + (p.ping?.avg || 0), 0) / pingProbes.length;
|
||||
const details = pingProbes.map((p) => `${p.city}/${p.network}: ${p.ping!.avg.toFixed(1)}ms`).join(', ');
|
||||
summary.push(`한국 Ping: 평균 ${avgPing.toFixed(1)}ms (${details})`);
|
||||
if (pingProbes.some((p) => (p.ping?.loss || 0) > 0)) {
|
||||
const lossy = pingProbes.filter((p) => (p.ping?.loss || 0) > 0);
|
||||
summary.push(`패킷 손실 감지: ${lossy.map((p) => `${p.city}: ${p.ping!.loss}%`).join(', ')}`);
|
||||
}
|
||||
}
|
||||
const httpProbes = koreaProbes.filter((p) => p.http);
|
||||
if (httpProbes.length > 0) {
|
||||
const avgTotal = httpProbes.reduce((sum, p) => sum + (p.http?.totalMs || 0), 0) / httpProbes.length;
|
||||
const details = httpProbes.map((p) => `${p.city}: ${p.http!.totalMs}ms(TTFB ${p.http!.firstByteMs}ms)`).join(', ');
|
||||
summary.push(`한국 HTTP 응답: 평균 ${avgTotal.toFixed(0)}ms (${details})`);
|
||||
}
|
||||
const errorProbes = koreaProbes.filter((p) => p.error);
|
||||
if (errorProbes.length > 0) {
|
||||
summary.push(`한국 프로브 오류: ${errorProbes.map((p) => `${p.city}: ${p.error}`).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
@@ -491,6 +680,7 @@ export interface NetworkDiagnosticOptions {
|
||||
skipIspCheck?: boolean;
|
||||
skipHttpCheck?: boolean;
|
||||
skipTcpCheck?: boolean;
|
||||
skipKoreaProbes?: boolean;
|
||||
}
|
||||
|
||||
export async function runNetworkDiagnostic(
|
||||
@@ -501,19 +691,21 @@ export async function runNetworkDiagnostic(
|
||||
const startTime = Date.now();
|
||||
|
||||
// Run all checks in parallel
|
||||
const [dns, ispBlocking, http, tcp] = await Promise.all([
|
||||
const [dns, ispBlocking, http, tcp, koreaProbes] = await Promise.all([
|
||||
resolveDoh(domain),
|
||||
options?.skipIspCheck ? Promise.resolve([]) : checkIspBlocking(domain),
|
||||
options?.skipHttpCheck ? Promise.resolve([]) : checkHttp(domain),
|
||||
options?.skipTcpCheck ? Promise.resolve([]) : checkTcpPorts(domain, options?.ports),
|
||||
options?.skipKoreaProbes ? Promise.resolve([]) : checkKoreaProbes(domain),
|
||||
]);
|
||||
|
||||
const summary = generateSummary(dns, ispBlocking, http, tcp);
|
||||
const summary = generateSummary(dns, ispBlocking, http, tcp, koreaProbes);
|
||||
|
||||
logger.info('네트워크 진단 완료', {
|
||||
domain,
|
||||
durationMs: Date.now() - startTime,
|
||||
summaryCount: summary.length,
|
||||
koreaProbeCount: koreaProbes.length,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -523,6 +715,7 @@ export async function runNetworkDiagnostic(
|
||||
ispBlocking,
|
||||
http,
|
||||
tcp,
|
||||
koreaProbes,
|
||||
summary,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user