Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
29 kB ยท 947 lines
TypeScript
at incident-api
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948import { and, count, eq, isNull } from "@openstatus/db";import { maintenance, maintenancesToPageComponents, monitor, page, pageComponent, pageComponentGroup, pageSubscriber, pageSubscriberToPageComponent, statusReport, statusReportUpdate, statusReportUpdateToPageComponents, statusReportsToPageComponents, worstImpact,} from "@openstatus/db/src/schema";import type { PageComponentImpact } from "@openstatus/db/src/schema";import type { PhaseResult, UpdateComponentImpact } from "@openstatus/importers";
import { emitAudit } from "../audit";import { type DB, type ServiceContext, tryGetActorUserId } from "../context";import { withComponentImpacts, withPageComponentIds,} from "../status-report/internal";import type { ImportProviderName } from "./schemas";import { clampPeriodicity, computePhaseStatus } from "./utils";
/** * Shared write-context threaded through every phase writer. Bundles the * service ctx (actor + workspace for audit attribution), the active * `tx`/db handle, and the provider name so per-resource audit rows carry * `source: "import"` + the source provider. */export type PhaseContext = { ctx: ServiceContext; tx: DB; provider: ImportProviderName;};
/** * Each phase writer mutates the phase in place: * - sets per-resource `status` ("created" | "skipped" | "failed"), * - stamps `openstatusId` on success, * - records the per-resource `error` string on failure, * - emits one audit row per *created* resource (skipped rows already * have their original create audit; failed rows have nothing to * attribute), * - rolls the phase-level `status` up from its resources. * * Writers never throw on a single-resource failure โ they only throw on * infrastructure errors (e.g. a required page record is missing). The * orchestrator in `run.ts` catches throws to abort the remaining phases. */
function auditMeta( pc: PhaseContext, extra?: Record<string, unknown>,): Record<string, unknown> { return { source: "import", provider: pc.provider, ...extra };}
export async function writePagePhase( pc: PhaseContext, phase: PhaseResult, existingPageId?: number,): Promise<number> { const { ctx, tx } = pc; const workspaceId = ctx.workspace.id; const limits = ctx.workspace.limits;
const resource = phase.resources[0]; if (!resource?.data) { throw new Error("No page data found in phase"); }
const data = resource.data as { workspaceId: number; title: string; description: string; slug: string; customDomain: string; published: boolean; icon: string; };
// Strip the custom domain when the plan doesn't allow one โ imports are // tolerant (skip/warn) rather than all-or-nothing. if (!limits["custom-domain"]) { data.customDomain = ""; }
if (existingPageId) { const existing = await tx .select() .from(page) .where( and(eq(page.id, existingPageId), eq(page.workspaceId, workspaceId)), ) .get();
if (!existing) { throw new Error( "Provided page not found or does not belong to workspace", ); }
await tx .update(page) .set({ title: data.title, description: data.description }) .where(eq(page.id, existingPageId));
resource.openstatusId = existingPageId; resource.status = "skipped"; phase.status = computePhaseStatus(phase.resources); return existingPageId; }
// Idempotency: slug is unique within a workspace. const existingBySlug = await tx .select() .from(page) .where(and(eq(page.slug, data.slug), eq(page.workspaceId, workspaceId))) .get();
if (existingBySlug) { resource.openstatusId = existingBySlug.id; resource.status = "skipped"; phase.status = computePhaseStatus(phase.resources); return existingBySlug.id; }
const [inserted] = await tx .insert(page) .values({ // `workspaceId` from ctx, not from provider-mapped `data`. Every // other phase writer (monitor / components / subscriber) already // uses the ctx-derived value; this keeps the authority consistent // and defends against the (unlikely) case where a provider mapper // round-trips the wrong workspace id into resource data. workspaceId, title: data.title, description: data.description, slug: data.slug, customDomain: data.customDomain, published: data.published, icon: data.icon, }) .returning();
if (!inserted) throw new Error("Failed to insert page");
resource.openstatusId = inserted.id; resource.status = "created"; phase.status = computePhaseStatus(phase.resources);
await emitAudit(tx, ctx, { action: "page.create", entityType: "page", entityId: inserted.id, metadata: auditMeta(pc, { sourceId: resource.sourceId, slug: data.slug }), after: inserted, });
return inserted.id;}
export async function writeComponentGroupsPhase( pc: PhaseContext, phase: PhaseResult, pageId: number, groupIdMap: Map<string, number>,): Promise<void> { const { ctx, tx } = pc; const workspaceId = ctx.workspace.id;
for (const resource of phase.resources) { try { const data = resource.data as { workspaceId: number; pageId: number; name: string; };
const existing = await tx .select() .from(pageComponentGroup) .where( and( eq(pageComponentGroup.name, data.name), eq(pageComponentGroup.pageId, pageId), ), ) .get();
if (existing) { groupIdMap.set(resource.sourceId, existing.id); resource.openstatusId = existing.id; resource.status = "skipped"; continue; }
const [inserted] = await tx .insert(pageComponentGroup) .values({ workspaceId, pageId, name: data.name }) .returning();
if (!inserted) { resource.status = "failed"; resource.error = "Insert returned no result"; continue; }
groupIdMap.set(resource.sourceId, inserted.id); resource.openstatusId = inserted.id; resource.status = "created";
await emitAudit(tx, ctx, { action: "page_component_group.create", entityType: "page_component_group", entityId: inserted.id, after: inserted, metadata: auditMeta(pc, { sourceId: resource.sourceId, pageId, name: data.name, }), }); } catch (err) { resource.status = "failed"; resource.error = err instanceof Error ? err.message : String(err); } }
phase.status = computePhaseStatus(phase.resources);}
export async function writeComponentsPhase( pc: PhaseContext, phase: PhaseResult, pageId: number, groupIdMap: Map<string, number>, componentIdMap: Map<string, number>, monitorIdMap?: Map<string, number>,): Promise<void> { const { ctx, tx } = pc; const workspaceId = ctx.workspace.id;
for (const resource of phase.resources) { if (resource.status === "skipped") continue;
try { const data = resource.data as { workspaceId: number; pageId: number; type: "static" | "monitor"; monitorId: number | null; sourceMonitorId?: string | null; name: string; description: string | null; order: number; sourceGroupId: string | null; };
if (data.type === "monitor") { if (data.sourceMonitorId && monitorIdMap) { data.monitorId = monitorIdMap.get(data.sourceMonitorId) ?? null; } if (!data.monitorId) { // Monitor wasn't imported โ usually because the monitors // phase was skipped (e.g. `options.includeMonitors === false`) // or the monitor itself failed to import. We fall back to a // static component so the component still lands, but flag // the degrade so the summary doesn't quietly report a // "created" monitor component with no monitor attached. // `resource.error` is set even though the resource will end // up `created` โ the other phase writers follow the same // convention when degrading. data.type = "static"; resource.error = data.sourceMonitorId ? `Source monitor ${data.sourceMonitorId} was not imported; created as static instead.` : "No source monitor available; created as static instead."; } }
const existing = await tx .select() .from(pageComponent) .where( and( eq(pageComponent.name, data.name), eq(pageComponent.pageId, pageId), ), ) .get();
if (existing) { componentIdMap.set(resource.sourceId, existing.id); resource.openstatusId = existing.id; resource.status = "skipped"; continue; }
const resolvedGroupId = data.sourceGroupId ? (groupIdMap.get(data.sourceGroupId) ?? null) : null;
const [inserted] = await tx .insert(pageComponent) .values({ workspaceId, pageId, type: data.type, monitorId: data.monitorId, name: data.name, description: data.description, order: data.order, groupId: resolvedGroupId, }) .returning();
if (!inserted) { resource.status = "failed"; resource.error = "Insert returned no result"; continue; }
componentIdMap.set(resource.sourceId, inserted.id); resource.openstatusId = inserted.id; resource.status = "created";
await emitAudit(tx, ctx, { action: "page_component.create", entityType: "page_component", entityId: inserted.id, metadata: auditMeta(pc, { sourceId: resource.sourceId, pageId, type: data.type, }), after: inserted, }); } catch (err) { resource.status = "failed"; resource.error = err instanceof Error ? err.message : String(err); } }
phase.status = computePhaseStatus(phase.resources);}
export async function writeIncidentsPhase( pc: PhaseContext, phase: PhaseResult, pageId: number, componentIdMap: Map<string, number>,): Promise<void> { const { ctx, tx } = pc; const workspaceId = ctx.workspace.id; const actorUserId = tryGetActorUserId(ctx.actor);
for (const resource of phase.resources) { try { const data = resource.data as { report: { title: string; status: "investigating" | "identified" | "monitoring" | "resolved"; workspaceId: number; pageId: number; }; updates: Array<{ status: "investigating" | "identified" | "monitoring" | "resolved"; message: string; date: Date; componentImpacts?: UpdateComponentImpact[]; }>; sourceComponentIds: string[]; };
// Idempotency by `(title, pageId)` โ every other phase writer // (page slug, monitor url, component name, subscriber email) // skips on duplicate before inserting, and imports are // explicitly re-runnable per `run.ts`'s phase-level recovery // model. Without this check, re-running an import would land // duplicate status reports on every pass. const existingReport = await tx .select({ id: statusReport.id }) .from(statusReport) .where( and( eq(statusReport.pageId, pageId), eq(statusReport.workspaceId, workspaceId), eq(statusReport.title, data.report.title), ), ) .get();
if (existingReport) { resource.openstatusId = existingReport.id; resource.status = "skipped"; // Reconcile component links on rerun. First run may have // written the report with a partial `componentIdMap` (some // components failed in their per-resource catch), so the link // set is a strict subset of `sourceComponentIds`. On rerun the // map is typically more complete โ reinsert with // `onConflictDoNothing` so the composite PK dedupes already- // written pairs and new ones land. const reconciliationLinks = data.sourceComponentIds .map((sourceId) => componentIdMap.get(sourceId)) .filter((id): id is number => id != null) .map((pageComponentId) => ({ statusReportId: existingReport.id, pageComponentId, })); if (reconciliationLinks.length > 0) { await tx .insert(statusReportsToPageComponents) .values(reconciliationLinks) .onConflictDoNothing(); } continue; }
const [insertedReport] = await tx .insert(statusReport) .values({ title: data.report.title, status: data.report.status, workspaceId, pageId, createdBy: actorUserId, updatedBy: actorUserId, }) .returning();
if (!insertedReport) { resource.status = "failed"; resource.error = "Failed to insert status report"; continue; }
// membership = sourceComponentIds โช impact-named components, upholding // the invariant that every impact row's component is in the report set const memberSourceIds = new Set([ ...data.sourceComponentIds, ...data.updates.flatMap((u) => (u.componentImpacts ?? []).map((ci) => ci.sourceComponentId), ), ]); // distinct source ids can collapse onto one internal component (the // components phase dedupes by name+page) โ dedupe to respect the PK const memberComponentIds = new Set<number>(); for (const sourceCompId of memberSourceIds) { const osCompId = componentIdMap.get(sourceCompId); if (osCompId) memberComponentIds.add(osCompId); } const componentLinks = [...memberComponentIds].map((pageComponentId) => ({ statusReportId: insertedReport.id, pageComponentId, }));
await emitAudit(tx, ctx, { action: "status_report.create", entityType: "status_report", entityId: insertedReport.id, metadata: auditMeta(pc, { sourceId: resource.sourceId, pageId, title: data.report.title, }), after: withPageComponentIds( insertedReport, componentLinks.map((l) => l.pageComponentId), ), });
// Insert updates one-by-one so each inserted id pairs with its source // update's componentImpacts deterministically (batch `.returning()` // order is not a guarantee worth relying on). for (const u of data.updates) { const row = await tx .insert(statusReportUpdate) .values({ status: u.status, message: u.message, date: u.date, statusReportId: insertedReport.id, createdBy: actorUserId, updatedBy: actorUserId, }) .returning() .get();
// ids missing from `componentIdMap` get no impact row โ same // partial-map caveat as the membership links. Collapsed source ids // (same internal component) keep the worst impact. const impactByComponent = new Map<number, PageComponentImpact>(); for (const ci of u.componentImpacts ?? []) { const osCompId = componentIdMap.get(ci.sourceComponentId); if (!osCompId) continue; const prev = impactByComponent.get(osCompId); impactByComponent.set( osCompId, prev ? worstImpact([prev, ci.impact]) : ci.impact, ); } const impactRows = [...impactByComponent].map( ([pageComponentId, impact]) => ({ statusReportUpdateId: row.id, pageComponentId, impact, }), ); if (impactRows.length > 0) { await tx .insert(statusReportUpdateToPageComponents) .values(impactRows); }
await emitAudit(tx, ctx, { action: "status_report_update.create", entityType: "status_report_update", entityId: row.id, metadata: auditMeta(pc, { sourceId: resource.sourceId, statusReportId: insertedReport.id, }), after: withComponentImpacts( row, impactRows.map(({ pageComponentId, impact }) => ({ pageComponentId, impact, })), ), }); }
if (componentLinks.length > 0) { await tx.insert(statusReportsToPageComponents).values(componentLinks); }
resource.openstatusId = insertedReport.id; resource.status = "created"; } catch (err) { resource.status = "failed"; resource.error = err instanceof Error ? err.message : String(err); } }
phase.status = computePhaseStatus(phase.resources);}
export async function writeMaintenancesPhase( pc: PhaseContext, phase: PhaseResult, pageId: number, componentIdMap: Map<string, number>,): Promise<void> { const { ctx, tx } = pc; const workspaceId = ctx.workspace.id; const actorUserId = tryGetActorUserId(ctx.actor);
for (const resource of phase.resources) { try { const data = resource.data as { title: string; message: string; from: Date; to: Date; workspaceId: number; pageId: number; sourceComponentIds: string[]; };
// Idempotency by `(title, pageId, from, to)` โ re-running an // import shouldn't create duplicate maintenance windows on each // pass. `title + pageId` alone could collide on unrelated future // maintenances sharing a name ("DB upgrade"), so the `from/to` // pair pins the match to a specific scheduled window. Same // reasoning as the status-report idempotency check above. const existing = await tx .select({ id: maintenance.id }) .from(maintenance) .where( and( eq(maintenance.pageId, pageId), eq(maintenance.workspaceId, workspaceId), eq(maintenance.title, data.title), eq(maintenance.from, data.from), eq(maintenance.to, data.to), ), ) .get();
if (existing) { resource.openstatusId = existing.id; resource.status = "skipped"; // Same reconciliation rationale as `writeIncidentsPhase` โ a // partial `componentIdMap` on the first run means the link // set may be incomplete, and the composite PK on // `(maintenanceId, pageComponentId)` makes the re-insert a // no-op for pairs that already exist. const reconciliationLinks = data.sourceComponentIds .map((sourceId) => componentIdMap.get(sourceId)) .filter((id): id is number => id != null) .map((pageComponentId) => ({ maintenanceId: existing.id, pageComponentId, })); if (reconciliationLinks.length > 0) { await tx .insert(maintenancesToPageComponents) .values(reconciliationLinks) .onConflictDoNothing(); } continue; }
const [inserted] = await tx .insert(maintenance) .values({ title: data.title, message: data.message, from: data.from, to: data.to, workspaceId, pageId, createdBy: actorUserId, updatedBy: actorUserId, }) .returning();
if (!inserted) { resource.status = "failed"; resource.error = "Failed to insert maintenance"; continue; }
const componentLinks: Array<{ maintenanceId: number; pageComponentId: number; }> = []; for (const sourceCompId of data.sourceComponentIds) { const osCompId = componentIdMap.get(sourceCompId); if (osCompId) { componentLinks.push({ maintenanceId: inserted.id, pageComponentId: osCompId, }); } } if (componentLinks.length > 0) { await tx.insert(maintenancesToPageComponents).values(componentLinks); }
resource.openstatusId = inserted.id; resource.status = "created";
await emitAudit(tx, ctx, { action: "maintenance.create", entityType: "maintenance", entityId: inserted.id, metadata: auditMeta(pc, { sourceId: resource.sourceId, pageId, title: data.title, }), after: inserted, }); } catch (err) { resource.status = "failed"; resource.error = err instanceof Error ? err.message : String(err); } }
phase.status = computePhaseStatus(phase.resources);}
export async function writeMonitorsPhase( pc: PhaseContext, phase: PhaseResult, monitorIdMap: Map<string, number>,): Promise<void> { const { ctx, tx } = pc; const workspaceId = ctx.workspace.id; const limits = ctx.workspace.limits;
const [monitorCount] = await tx .select({ count: count() }) .from(monitor) .where( and(eq(monitor.workspaceId, workspaceId), isNull(monitor.deletedAt)), ); const maxMonitors = limits.monitors; const remaining = maxMonitors - (monitorCount?.count ?? 0);
if (remaining <= 0) { for (const resource of phase.resources) { resource.status = "skipped"; resource.error = `Skipped: monitor limit reached (${maxMonitors})`; } phase.status = computePhaseStatus(phase.resources); return; }
let importedCount = 0; for (const resource of phase.resources) { if (importedCount >= remaining) { resource.status = "skipped"; resource.error = `Skipped: would exceed monitor limit (${maxMonitors})`; continue; }
try { const data = resource.data as { workspaceId: number; jobType: string; periodicity: string; status: string; active: boolean; regions: string; url: string; name: string; description: string; headers: string; body: string; method: string; timeout: number; followRedirects?: boolean; sourceMonitorGroupId: string | null; };
// Idempotency: same url in the workspace (active only). const existing = await tx .select() .from(monitor) .where( and( eq(monitor.url, data.url), eq(monitor.workspaceId, workspaceId), isNull(monitor.deletedAt), ), ) .get();
if (existing) { monitorIdMap.set(resource.sourceId, existing.id); resource.openstatusId = existing.id; resource.status = "skipped"; continue; }
const periodicity = clampPeriodicity( data.periodicity, limits.periodicity, );
const [inserted] = await tx .insert(monitor) .values({ workspaceId, jobType: data.jobType as | "http" | "tcp" | "icmp" | "grpc" | "udp" | "dns" | "ssl", periodicity: periodicity as | "30s" | "1m" | "5m" | "10m" | "30m" | "1h" | "other", status: "active", active: data.active, regions: data.regions, url: data.url, name: data.name, description: data.description, headers: data.headers, body: data.body, method: data.method as | "GET" | "POST" | "HEAD" | "PUT" | "PATCH" | "DELETE" | "TRACE" | "CONNECT" | "OPTIONS", timeout: data.timeout, followRedirects: data.followRedirects, }) .returning();
if (!inserted) { resource.status = "failed"; resource.error = "Insert returned no result"; continue; }
monitorIdMap.set(resource.sourceId, inserted.id); resource.openstatusId = inserted.id; resource.status = "created"; importedCount++;
await emitAudit(tx, ctx, { action: "monitor.create", entityType: "monitor", entityId: inserted.id, metadata: auditMeta(pc, { sourceId: resource.sourceId, url: data.url, jobType: data.jobType, }), after: inserted, }); } catch (err) { resource.status = "failed"; resource.error = err instanceof Error ? err.message : String(err); } }
phase.status = computePhaseStatus(phase.resources);}
// TODO: migrate to new `pageSubscription` + `pageSubscriptionToPageComponent` tablesexport async function writeSubscribersPhase( pc: PhaseContext, phase: PhaseResult, pageId: number, componentIdMap: Map<string, number>,): Promise<void> { const { ctx, tx } = pc;
for (const resource of phase.resources) { try { const data = resource.data as { email: string; pageId: number; confirmed: boolean; sourceComponentIds: string[]; };
const email = data.email.toLowerCase();
const existing = await tx .select() .from(pageSubscriber) .where( and( eq(pageSubscriber.email, email), eq(pageSubscriber.pageId, pageId), eq(pageSubscriber.channelType, "email"), ), ) .get();
if (existing) { resource.openstatusId = existing.id; resource.status = "skipped"; // Same reconciliation rationale as `writeIncidentsPhase` โ a // partial `componentIdMap` on the first run means the link // set may be incomplete, and the composite PK on // `(pageSubscriberId, pageComponentId)` makes the re-insert a // no-op for pairs that already exist. const reconciliationLinks = data.sourceComponentIds .map((sourceId) => componentIdMap.get(sourceId)) .filter((id): id is number => id != null) .map((pageComponentId) => ({ pageSubscriberId: existing.id, pageComponentId, })); if (reconciliationLinks.length > 0) { await tx .insert(pageSubscriberToPageComponent) .values(reconciliationLinks) .onConflictDoNothing(); } continue; }
const [inserted] = await tx .insert(pageSubscriber) .values({ email, pageId, channelType: "email", source: "import", token: crypto.randomUUID(), acceptedAt: data.confirmed ? new Date() : undefined, }) .returning();
if (!inserted) { resource.status = "failed"; resource.error = "Insert returned no result"; continue; }
const componentLinks: Array<{ pageSubscriberId: number; pageComponentId: number; }> = []; for (const sourceCompId of data.sourceComponentIds) { const osCompId = componentIdMap.get(sourceCompId); if (osCompId) { componentLinks.push({ pageSubscriberId: inserted.id, pageComponentId: osCompId, }); } } if (componentLinks.length > 0) { await tx.insert(pageSubscriberToPageComponent).values(componentLinks); }
resource.openstatusId = inserted.id; resource.status = "created";
// Strip `token` โ it's the unsubscribe / management capability // link. Everything else on the row is fine to audit. const { token: _token, ...safe } = inserted; await emitAudit(tx, ctx, { action: "page_subscriber.create", entityType: "page_subscriber", entityId: inserted.id, metadata: auditMeta(pc, { sourceId: resource.sourceId, pageId, email, }), after: safe, }); } catch (err) { resource.status = "failed"; resource.error = err instanceof Error ? err.message : String(err); } }
phase.status = computePhaseStatus(phase.resources);}