mirror of
https://github.com/jakejarvis/hoot.git
synced 2025-10-18 20:14:25 -04:00
Refactor domain router to use loggedProcedure for enhanced logging and observability
This commit is contained in:
@@ -17,7 +17,7 @@ import { detectHosting } from "@/server/services/hosting";
|
||||
import { getRegistration } from "@/server/services/registration";
|
||||
import { getOrCreateScreenshotBlobUrl } from "@/server/services/screenshot";
|
||||
import { getSeo } from "@/server/services/seo";
|
||||
import { createTRPCRouter, publicProcedure } from "@/trpc/init";
|
||||
import { createTRPCRouter, loggedProcedure } from "@/trpc/init";
|
||||
|
||||
export const domainInput = z
|
||||
.object({ domain: z.string().min(1) })
|
||||
@@ -28,34 +28,34 @@ export const domainInput = z
|
||||
});
|
||||
|
||||
export const domainRouter = createTRPCRouter({
|
||||
registration: publicProcedure
|
||||
registration: loggedProcedure
|
||||
.input(domainInput)
|
||||
.output(RegistrationSchema)
|
||||
.query(({ input }) => getRegistration(input.domain)),
|
||||
dns: publicProcedure
|
||||
dns: loggedProcedure
|
||||
.input(domainInput)
|
||||
.output(DnsResolveResultSchema)
|
||||
.query(({ input }) => resolveAll(input.domain)),
|
||||
hosting: publicProcedure
|
||||
hosting: loggedProcedure
|
||||
.input(domainInput)
|
||||
.output(HostingSchema)
|
||||
.query(({ input }) => detectHosting(input.domain)),
|
||||
certificates: publicProcedure
|
||||
certificates: loggedProcedure
|
||||
.input(domainInput)
|
||||
.output(CertificatesSchema)
|
||||
.query(({ input }) => getCertificates(input.domain)),
|
||||
headers: publicProcedure
|
||||
headers: loggedProcedure
|
||||
.input(domainInput)
|
||||
.output(HttpHeadersSchema)
|
||||
.query(({ input }) => probeHeaders(input.domain)),
|
||||
seo: publicProcedure
|
||||
seo: loggedProcedure
|
||||
.input(domainInput)
|
||||
.output(SeoResponseSchema)
|
||||
.query(({ input }) => getSeo(input.domain)),
|
||||
favicon: publicProcedure
|
||||
favicon: loggedProcedure
|
||||
.input(domainInput)
|
||||
.query(({ input }) => getOrCreateFaviconBlobUrl(input.domain)),
|
||||
screenshot: publicProcedure
|
||||
screenshot: loggedProcedure
|
||||
.input(domainInput)
|
||||
.query(({ input }) => getOrCreateScreenshotBlobUrl(input.domain)),
|
||||
});
|
||||
|
@@ -8,8 +8,15 @@ export async function getCertificates(domain: string): Promise<Certificate[]> {
|
||||
const lower = domain.toLowerCase();
|
||||
const key = ns("tls", lower);
|
||||
|
||||
console.debug("[certificates] start", { domain: lower });
|
||||
const cached = await redis.get<Certificate[]>(key);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
console.info("[certificates] cache hit", {
|
||||
domain: lower,
|
||||
count: cached.length,
|
||||
});
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Client gating avoids calling this without A/AAAA; server does not pre-check DNS here.
|
||||
|
||||
@@ -76,8 +83,17 @@ export async function getCertificates(domain: string): Promise<Certificate[]> {
|
||||
|
||||
const ttl = out.length > 0 ? 12 * 60 * 60 : 10 * 60;
|
||||
await redis.set(key, out, { ex: ttl });
|
||||
console.info("[certificates] ok", {
|
||||
domain: lower,
|
||||
chain_length: out.length,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
});
|
||||
return out;
|
||||
} catch (err) {
|
||||
console.warn("[certificates] error", {
|
||||
domain: lower,
|
||||
error: (err as Error)?.message,
|
||||
});
|
||||
await captureServer("tls_probe", {
|
||||
domain: lower,
|
||||
chain_length: 0,
|
||||
|
@@ -47,6 +47,7 @@ export const DOH_PROVIDERS: DohProvider[] = [
|
||||
export async function resolveAll(domain: string): Promise<DnsResolveResult> {
|
||||
const lower = domain.toLowerCase();
|
||||
const startedAt = Date.now();
|
||||
console.debug("[dns] start", { domain: lower });
|
||||
const providers = providerOrderForLookup(lower);
|
||||
const durationByProvider: Record<string, number> = {};
|
||||
let lastError: unknown = null;
|
||||
@@ -85,6 +86,11 @@ export async function resolveAll(domain: string): Promise<DnsResolveResult> {
|
||||
duration_ms_by_provider: {},
|
||||
cache_hit: true,
|
||||
});
|
||||
console.info("[dns] cache hit", {
|
||||
domain: lower,
|
||||
counts,
|
||||
resolver: resolverUsed,
|
||||
});
|
||||
return { records: flat, resolver: resolverUsed } as DnsResolveResult;
|
||||
}
|
||||
|
||||
@@ -138,8 +144,19 @@ export async function resolveAll(domain: string): Promise<DnsResolveResult> {
|
||||
duration_ms_by_provider: durationByProvider,
|
||||
cache_hit: !usedFresh,
|
||||
});
|
||||
console.info("[dns] ok", {
|
||||
domain: lower,
|
||||
counts,
|
||||
resolver: resolverUsed,
|
||||
duration_ms_total: Date.now() - startedAt,
|
||||
});
|
||||
return { records: flat, resolver: resolverUsed } as DnsResolveResult;
|
||||
} catch (err) {
|
||||
console.warn("[dns] provider attempt failed", {
|
||||
domain: lower,
|
||||
provider: provider.key,
|
||||
error: (err as Error)?.message,
|
||||
});
|
||||
durationByProvider[provider.key] = Date.now() - attemptStart;
|
||||
lastError = err;
|
||||
// Try next provider in rotation
|
||||
@@ -153,6 +170,11 @@ export async function resolveAll(domain: string): Promise<DnsResolveResult> {
|
||||
failure: true,
|
||||
provider_attempts: providers.length,
|
||||
});
|
||||
console.error("[dns] all providers failed", {
|
||||
domain: lower,
|
||||
providers: providers.map((p) => p.key),
|
||||
error: String(lastError),
|
||||
});
|
||||
throw new Error(
|
||||
`All DoH providers failed for ${lower}: ${String(lastError)}`,
|
||||
);
|
||||
|
@@ -7,8 +7,15 @@ export async function probeHeaders(domain: string): Promise<HttpHeader[]> {
|
||||
const url = `https://${domain}/`;
|
||||
const key = ns("headers", lower);
|
||||
|
||||
console.debug("[headers] start", { domain: lower });
|
||||
const cached = await redis.get<HttpHeader[]>(key);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
console.info("[headers] cache hit", {
|
||||
domain: lower,
|
||||
count: cached.length,
|
||||
});
|
||||
return cached;
|
||||
}
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 5000;
|
||||
try {
|
||||
@@ -63,8 +70,17 @@ export async function probeHeaders(domain: string): Promise<HttpHeader[]> {
|
||||
});
|
||||
|
||||
await redis.set(key, normalized, { ex: 10 * 60 });
|
||||
console.info("[headers] ok", {
|
||||
domain: lower,
|
||||
status: final.status,
|
||||
count: normalized.length,
|
||||
});
|
||||
return normalized;
|
||||
} catch (err) {
|
||||
console.warn("[headers] error", {
|
||||
domain: lower,
|
||||
error: (err as Error)?.message,
|
||||
});
|
||||
await captureServer("headers_probe", {
|
||||
domain: lower,
|
||||
status: -1,
|
||||
|
@@ -12,9 +12,16 @@ import { probeHeaders } from "@/server/services/headers";
|
||||
import { lookupIpMeta } from "@/server/services/ip";
|
||||
|
||||
export async function detectHosting(domain: string): Promise<Hosting> {
|
||||
const startedAt = Date.now();
|
||||
console.debug("[hosting] start", { domain });
|
||||
|
||||
const key = ns("hosting", domain.toLowerCase());
|
||||
const cached = await redis.get<Hosting>(key);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
console.info("[hosting] cache hit", { domain });
|
||||
return cached;
|
||||
}
|
||||
|
||||
const { records: dns } = await resolveAll(domain);
|
||||
const a = dns.find((d) => d.type === "A");
|
||||
const mx = dns.filter((d) => d.type === "MX");
|
||||
@@ -100,7 +107,15 @@ export async function detectHosting(domain: string): Promise<Hosting> {
|
||||
dns_provider: dnsName,
|
||||
ip_present: Boolean(ip),
|
||||
geo_country: geo.country || "",
|
||||
duration_ms: Date.now() - startedAt,
|
||||
});
|
||||
await redis.set(key, info, { ex: 24 * 60 * 60 });
|
||||
console.info("[hosting] ok", {
|
||||
domain,
|
||||
hosting: hostingName,
|
||||
email: emailName,
|
||||
dns_provider: dnsName,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
});
|
||||
return info;
|
||||
}
|
||||
|
@@ -10,9 +10,12 @@ export async function lookupIpMeta(ip: string): Promise<{
|
||||
owner: string | null;
|
||||
domain: string | null;
|
||||
}> {
|
||||
const startedAt = Date.now();
|
||||
console.debug("[ip] start", { ip });
|
||||
try {
|
||||
const res = await fetch(`https://ipwho.is/${encodeURIComponent(ip)}`);
|
||||
if (!res.ok) throw new Error("ipwho fail");
|
||||
if (!res.ok) throw new Error("ipwho.is fail");
|
||||
|
||||
const j = (await res.json()) as {
|
||||
city?: string;
|
||||
region?: string;
|
||||
@@ -23,6 +26,9 @@ export async function lookupIpMeta(ip: string): Promise<{
|
||||
flag?: { emoji?: string };
|
||||
connection?: { org?: string; isp?: string; domain?: string };
|
||||
};
|
||||
|
||||
console.debug("[ip] ipwho.is result", { ip, json: j });
|
||||
|
||||
const geo = {
|
||||
city: j.city || "",
|
||||
region: j.region || j.state || "",
|
||||
@@ -35,8 +41,17 @@ export async function lookupIpMeta(ip: string): Promise<{
|
||||
const isp = j.connection?.isp?.trim();
|
||||
const owner = (org || isp || "").trim() || null;
|
||||
const domain = (j.connection?.domain || "").trim() || null;
|
||||
|
||||
console.info("[ip] ok", {
|
||||
ip,
|
||||
owner: owner || undefined,
|
||||
domain: domain || undefined,
|
||||
country: geo.country,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
});
|
||||
return { geo, owner, domain };
|
||||
} catch {
|
||||
console.warn("[ip] error", { ip });
|
||||
return {
|
||||
geo: {
|
||||
city: "",
|
||||
|
@@ -11,20 +11,28 @@ import type { Registration } from "@/lib/schemas";
|
||||
// Type exported from schemas; keep alias for local file consumers if any
|
||||
|
||||
export async function getRegistration(domain: string): Promise<Registration> {
|
||||
const startedAt = Date.now();
|
||||
console.debug("[registration] start", { domain });
|
||||
|
||||
const registrable = toRegistrableDomain(domain);
|
||||
if (!registrable) throw new Error("Invalid domain");
|
||||
|
||||
const key = ns("reg", registrable.toLowerCase());
|
||||
const cached = await redis.get<Registration>(key);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
console.info("[registration] cache hit", { domain: registrable });
|
||||
return cached;
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
const { ok, record, error } = await lookupDomain(registrable, {
|
||||
timeoutMs: 15000,
|
||||
followWhoisReferral: true,
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
|
||||
if (!ok || !record) {
|
||||
console.warn("[registration] error", {
|
||||
domain: registrable,
|
||||
error: error || "unknown",
|
||||
});
|
||||
await captureServer("registration_lookup", {
|
||||
domain: registrable,
|
||||
outcome: "error",
|
||||
@@ -34,6 +42,11 @@ export async function getRegistration(domain: string): Promise<Registration> {
|
||||
throw new Error(error || "Registration lookup failed");
|
||||
}
|
||||
|
||||
// Log raw rdapper record for observability (safe; already public data)
|
||||
console.debug("[registration] rdapper result", {
|
||||
...record,
|
||||
});
|
||||
|
||||
const ttl = record.isRegistered ? 24 * 60 * 60 : 60 * 60;
|
||||
let registrarName = (record.registrar?.name || "").toString();
|
||||
let registrarDomain: string | null = null;
|
||||
@@ -66,6 +79,12 @@ export async function getRegistration(domain: string): Promise<Registration> {
|
||||
duration_ms: Date.now() - startedAt,
|
||||
source: record.source,
|
||||
});
|
||||
console.info("[registration] ok", {
|
||||
domain: registrable,
|
||||
registered: record.isRegistered,
|
||||
registrar: withProvider.registrarProvider.name,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
});
|
||||
|
||||
return withProvider;
|
||||
}
|
||||
|
@@ -37,8 +37,16 @@ export async function getSeo(domain: string): Promise<SeoResponse> {
|
||||
const metaKey = ns(`${baseKey}:meta`, "v1");
|
||||
const robotsKey = ns(`${baseKey}:robots`, "v1");
|
||||
|
||||
console.debug("[seo] start", { domain: lower });
|
||||
const cached = await redis.get<SeoResponse>(metaKey);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
console.info("[seo] cache hit", {
|
||||
domain: lower,
|
||||
has_meta: !!cached.meta,
|
||||
has_robots: !!cached.robots,
|
||||
});
|
||||
return cached;
|
||||
}
|
||||
|
||||
let finalUrl: string | null = `https://${lower}/`;
|
||||
let status: number | null = null;
|
||||
@@ -150,6 +158,14 @@ export async function getSeo(domain: string): Promise<SeoResponse> {
|
||||
has_errors: Boolean(htmlError || robotsError),
|
||||
});
|
||||
|
||||
console.info("[seo] ok", {
|
||||
domain: lower,
|
||||
status: status ?? -1,
|
||||
has_meta: !!meta,
|
||||
has_robots: !!robots,
|
||||
has_errors: Boolean(htmlError || robotsError),
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
17
trpc/init.ts
17
trpc/init.ts
@@ -14,3 +14,20 @@ const t = initTRPC.context<Context>().create({
|
||||
export const createTRPCRouter = t.router;
|
||||
export const createCallerFactory = t.createCallerFactory;
|
||||
export const publicProcedure = t.procedure;
|
||||
|
||||
export const loggedProcedure = publicProcedure.use(async (opts) => {
|
||||
const start = Date.now();
|
||||
const result = await opts.next();
|
||||
const durationMs = Date.now() - start;
|
||||
const meta = {
|
||||
path: opts.path,
|
||||
type: opts.type,
|
||||
durationMs,
|
||||
};
|
||||
if (result.ok) {
|
||||
console.info("[trpc] ok", meta);
|
||||
} else {
|
||||
console.error("[trpc] error", meta);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
Reference in New Issue
Block a user