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.
13 kB ยท 460 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461import { Events } from "@openstatus/analytics";import { headerAssertion, jsonBodyAssertion, recordAssertion, statusAssertion, textBodyAssertion,} from "@openstatus/assertions";import { NotFoundError } from "@openstatus/services";import { type CreateMonitorInput, type UpdateMonitorGeneralInput, bulkUpdateMonitors, cloneMonitor, createMonitor, deleteMonitor, deleteMonitors, getMonitor, grpcTlsModes, listMonitors, monitorJobTypes, monitorMethods, monitorPeriodicity, updateMonitorFollowRedirects, updateMonitorGeneral, updateMonitorNotifiers, updateMonitorOtel, updateMonitorPublic, updateMonitorResponseTime, updateMonitorRetry, updateMonitorSchedulingRegions, updateMonitorTags,} from "@openstatus/services/monitor";import { headerPairSchema } from "@openstatus/utils";import { z } from "zod";
import { env } from "../env";import { toServiceCtx, toTRPCError } from "../service-adapter";import { createTRPCRouter, protectedProcedure } from "../trpc";import { testDns, testGrpc, testHttp, testIcmp, testTcp } from "./checker";
// self-host has no access to the openstatus checker fleet, so the pre-save// endpoint test can never succeed โ skip it entirely.const isSelfHost = env.SELF_HOST;
// tRPC-side input schemas. These preserve the existing wire contract exactly.
const headerPair = headerPairSchema;const assertionUnion = z.discriminatedUnion("type", [ statusAssertion, headerAssertion, textBodyAssertion, jsonBodyAssertion, recordAssertion,]);
const newMonitorTRPCInput = z.object({ name: z.string(), jobType: z.enum(monitorJobTypes), url: z.string(), method: z.enum(monitorMethods), headers: z.array(headerPair), body: z.string().optional(), assertions: z.array(assertionUnion), active: z.boolean().prefault(false), saveCheck: z.boolean().prefault(false), skipCheck: z.boolean().prefault(false), grpcService: z.string().optional(), grpcTls: z.enum(grpcTlsModes).optional(),});
const updateGeneralTRPCInput = z.object({ id: z.number(), jobType: z.enum(monitorJobTypes), url: z.string(), method: z.enum(monitorMethods), headers: z.array(headerPair), body: z.string().optional(), name: z.string(), assertions: z.array(assertionUnion), active: z.boolean().prefault(true), skipCheck: z.boolean().prefault(true), saveCheck: z.boolean().prefault(false), grpcService: z.string().optional(), grpcTls: z.enum(grpcTlsModes).optional(),});
export const monitorRouter = createTRPCRouter({ delete: protectedProcedure .meta({ track: Events.DeleteMonitor }) .input(z.object({ id: z.number() })) .mutation(async ({ ctx, input }) => { try { await deleteMonitor({ ctx: toServiceCtx(ctx), input: { id: input.id }, }); } catch (err) { // Preserve the pre-migration idempotent behaviour โ old code // silently returned when the row was already gone. if (err instanceof NotFoundError) return; toTRPCError(err); } }),
deleteMonitors: protectedProcedure .input(z.object({ ids: z.number().array() })) .mutation(async ({ ctx, input }) => { try { await deleteMonitors({ ctx: toServiceCtx(ctx), input: { ids: input.ids }, }); } catch (err) { toTRPCError(err); } }),
updateMonitors: protectedProcedure .input( z.object({ ids: z.number().array(), public: z.boolean().optional(), active: z.boolean().optional(), }), ) .mutation(async ({ ctx, input }) => { try { await bulkUpdateMonitors({ ctx: toServiceCtx(ctx), input: { ids: input.ids, public: input.public, active: input.active, }, }); } catch (err) { toTRPCError(err); } }),
list: protectedProcedure .input(z.object({ order: z.enum(["asc", "desc"]).optional() }).optional()) .query(async ({ ctx, input }) => { try { const { items } = await listMonitors({ ctx: toServiceCtx(ctx), input: { // tRPC consumers (dashboard) want the full set; no paging UI. limit: 10_000, offset: 0, order: input?.order ?? "desc", }, }); return items; } catch (err) { toTRPCError(err); } }),
get: protectedProcedure .input(z.object({ id: z.coerce.number() })) .query(async ({ ctx, input }) => { try { return await getMonitor({ ctx: toServiceCtx(ctx), input: { id: input.id }, }); } catch (err) { toTRPCError(err); } }),
clone: protectedProcedure .meta({ track: Events.CloneMonitor }) .input(z.object({ id: z.number() })) .mutation(async ({ ctx, input }) => { try { return await cloneMonitor({ ctx: toServiceCtx(ctx), input: { id: input.id }, }); } catch (err) { toTRPCError(err); } }),
updateRetry: protectedProcedure .meta({ track: Events.UpdateMonitor }) .input(z.object({ id: z.number(), retry: z.number() })) .mutation(async ({ ctx, input }) => { try { await updateMonitorRetry({ ctx: toServiceCtx(ctx), input: { id: input.id, retry: input.retry }, }); } catch (err) { toTRPCError(err); } }),
updateFollowRedirects: protectedProcedure .meta({ track: Events.UpdateMonitor }) .input(z.object({ id: z.number(), followRedirects: z.boolean() })) .mutation(async ({ ctx, input }) => { try { await updateMonitorFollowRedirects({ ctx: toServiceCtx(ctx), input: { id: input.id, followRedirects: input.followRedirects, }, }); } catch (err) { toTRPCError(err); } }),
updateOtel: protectedProcedure .meta({ track: Events.UpdateMonitor }) .input( z.object({ id: z.number(), otelEndpoint: z.string(), otelHeaders: z.array(headerPair).optional(), }), ) .mutation(async ({ ctx, input }) => { try { await updateMonitorOtel({ ctx: toServiceCtx(ctx), input: { id: input.id, otelEndpoint: input.otelEndpoint, otelHeaders: input.otelHeaders, }, }); } catch (err) { toTRPCError(err); } }),
updatePublic: protectedProcedure .meta({ track: Events.UpdateMonitor }) .input(z.object({ id: z.number(), public: z.boolean() })) .mutation(async ({ ctx, input }) => { try { await updateMonitorPublic({ ctx: toServiceCtx(ctx), input: { id: input.id, public: input.public }, }); } catch (err) { toTRPCError(err); } }),
updateSchedulingRegions: protectedProcedure .meta({ track: Events.UpdateMonitor }) .input( z.object({ id: z.number(), regions: z.array(z.string()), periodicity: z.enum(monitorPeriodicity), privateLocations: z.array(z.number()), }), ) .mutation(async ({ ctx, input }) => { try { await updateMonitorSchedulingRegions({ ctx: toServiceCtx(ctx), input: { id: input.id, regions: input.regions, periodicity: input.periodicity, privateLocations: input.privateLocations, }, }); } catch (err) { toTRPCError(err); } }),
updateResponseTime: protectedProcedure .meta({ track: Events.UpdateMonitor }) .input( z.object({ id: z.number(), timeout: z.number(), degradedAfter: z.number().nullish(), }), ) .mutation(async ({ ctx, input }) => { try { await updateMonitorResponseTime({ ctx: toServiceCtx(ctx), input: { id: input.id, timeout: input.timeout, degradedAfter: input.degradedAfter, }, }); } catch (err) { toTRPCError(err); } }),
updateTags: protectedProcedure .meta({ track: Events.UpdateMonitor }) .input(z.object({ id: z.number(), tags: z.array(z.number()) })) .mutation(async ({ ctx, input }) => { try { await updateMonitorTags({ ctx: toServiceCtx(ctx), input: { id: input.id, tags: input.tags }, }); } catch (err) { toTRPCError(err); } }),
updateGeneral: protectedProcedure .meta({ track: Events.UpdateMonitor }) .input(updateGeneralTRPCInput) .mutation(async ({ ctx, input }) => { try { // Pre-save endpoint check โ kept at the tRPC layer because the // `testHttp` / `testTcp` / `testDns` helpers hit external URLs and // are tRPC-specific UX; services unconditionally save. if (!isSelfHost && !input.skipCheck && input.active) { if (input.jobType === "http") { await testHttp({ url: input.url, method: input.method, headers: input.headers, body: input.body, assertions: input.assertions.filter( (a) => a.type !== "dnsRecord", ), region: "ams", }); } else if (input.jobType === "tcp") { await testTcp({ url: input.url, region: "ams" }); } else if (input.jobType === "dns") { await testDns({ url: input.url, region: "ams", assertions: input.assertions.filter( (a) => a.type === "dnsRecord", ), }); } else if (input.jobType === "icmp") { await testIcmp({ url: input.url, region: "ams" }); } else if (input.jobType === "grpc") { await testGrpc({ url: input.url, service: input.grpcService, tls: input.grpcTls ?? "tls", headers: input.headers, region: "ams", }); } }
const serviceInput: UpdateMonitorGeneralInput = { id: input.id, name: input.name, jobType: input.jobType, url: input.url, method: input.method, headers: input.headers, body: input.body, assertions: input.assertions, active: input.active, grpcService: input.grpcService, grpcTls: input.grpcTls, }; await updateMonitorGeneral({ ctx: toServiceCtx(ctx), input: serviceInput, }); } catch (err) { toTRPCError(err); } }),
updateNotifiers: protectedProcedure .meta({ track: Events.UpdateMonitor }) .input(z.object({ id: z.number(), notifiers: z.array(z.number()) })) .mutation(async ({ ctx, input }) => { try { await updateMonitorNotifiers({ ctx: toServiceCtx(ctx), input: { id: input.id, notifiers: input.notifiers }, }); } catch (err) { toTRPCError(err); } }),
new: protectedProcedure .meta({ track: Events.CreateMonitor, trackProps: ["url", "jobType"] }) .input(newMonitorTRPCInput) .mutation(async ({ ctx, input }) => { try { if (!isSelfHost && !input.skipCheck) { if (input.jobType === "http") { await testHttp({ url: input.url, method: input.method, headers: input.headers, body: input.body, assertions: input.assertions.filter( (a) => a.type !== "dnsRecord", ), region: "ams", }); } else if (input.jobType === "tcp") { await testTcp({ url: input.url, region: "ams" }); } else if (input.jobType === "dns") { await testDns({ url: input.url, region: "ams", assertions: input.assertions.filter( (a) => a.type === "dnsRecord", ), }); } else if (input.jobType === "icmp") { await testIcmp({ url: input.url, region: "ams" }); } else if (input.jobType === "grpc") { await testGrpc({ url: input.url, service: input.grpcService, tls: input.grpcTls ?? "tls", headers: input.headers, region: "ams", }); } }
const serviceInput: CreateMonitorInput = { name: input.name, jobType: input.jobType, url: input.url, method: input.method, headers: input.headers, body: input.body, assertions: input.assertions, active: input.active, grpcService: input.grpcService, grpcTls: input.grpcTls, }; return await createMonitor({ ctx: toServiceCtx(ctx), input: serviceInput, }); } catch (err) { toTRPCError(err); } }),});