1
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:
2025-10-09 01:05:08 -04:00
parent 96ce5ad455
commit 9110ed9741
9 changed files with 154 additions and 18 deletions

View File

@@ -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)),
});

View File

@@ -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,

View File

@@ -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)}`,
);

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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: "",

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
});