Openstatus
www.openstatus.dev
1import { z } from "zod";
2
3import { monitorRegions } from "@openstatus/db/src/schema/constants";
4import { OSTinybird } from "@openstatus/tinybird";
5
6import { type SQL, and, db, eq, inArray } from "@openstatus/db";
7import { monitor } from "@openstatus/db/src/schema";
8import { TRPCError } from "@trpc/server";
9import { env } from "../../env";
10import { createTRPCRouter, protectedProcedure } from "../../trpc";
11import { calculatePeriod } from "./utils";
12
13const tb = new OSTinybird(env.TINY_BIRD_API_KEY);
14
15const periods = ["1d", "7d", "14d"] as const;
16const types = ["http", "tcp", "dns"] as const;
17type Period = (typeof periods)[number];
18type Type = (typeof types)[number];
19
20// NEW: workspace-level counters helper
21export function getWorkspace30dProcedure(type: Type) {
22 return type === "http" ? tb.httpWorkspace30d : tb.tcpWorkspace30d;
23}
24// Helper functions to get the right procedure based on period and type
25export function getListProcedure(period: Period, type: Type) {
26 console.log({ period, type });
27 switch (period) {
28 case "1d":
29 if (type === "http") return tb.httpListDaily;
30 if (type === "tcp") return tb.tcpListDaily;
31 if (type === "dns") return tb.dnsListBiweekly;
32 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
33 case "7d":
34 if (type === "http") return tb.httpListWeekly;
35 if (type === "tcp") return tb.tcpListWeekly;
36 if (type === "dns") return tb.dnsListBiweekly;
37 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
38 case "14d":
39 if (type === "http") return tb.httpListBiweekly;
40 if (type === "tcp") return tb.tcpListBiweekly;
41 if (type === "dns") return tb.dnsListBiweekly;
42 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
43 default:
44 if (type === "http") return tb.httpListDaily;
45 if (type === "tcp") return tb.tcpListDaily;
46 if (type === "dns") return tb.dnsListBiweekly;
47 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
48 }
49}
50
51export function getMetricsProcedure(period: Period, type: Type) {
52 switch (period) {
53 case "1d":
54 if (type === "dns") return tb.dnsMetricsDaily;
55 if (type === "http") return tb.httpMetricsDaily;
56 if (type === "tcp") return tb.tcpMetricsDaily;
57 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
58 case "7d":
59 if (type === "dns") return tb.dnsMetricsWeekly;
60 if (type === "http") return tb.httpMetricsWeekly;
61 if (type === "tcp") return tb.tcpMetricsWeekly;
62 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
63 case "14d":
64 if (type === "dns") return tb.dnsMetricsBiweekly;
65 if (type === "http") return tb.httpMetricsBiweekly;
66 if (type === "tcp") return tb.tcpMetricsBiweekly;
67 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
68 default:
69 if (type === "dns") return tb.dnsMetricsDaily;
70 if (type === "http") return tb.httpMetricsDaily;
71 if (type === "tcp") return tb.tcpMetricsDaily;
72 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
73 }
74}
75
76export function getMetricsByRegionProcedure(period: Period, type: Type) {
77 switch (period) {
78 case "1d":
79 return type === "http"
80 ? tb.httpMetricsByRegionDaily
81 : tb.tcpMetricsByRegionDaily;
82 case "7d":
83 return type === "http"
84 ? tb.httpMetricsByRegionWeekly
85 : tb.tcpMetricsByRegionWeekly;
86 case "14d":
87 return type === "http"
88 ? tb.httpMetricsByRegionBiweekly
89 : tb.tcpMetricsByRegionBiweekly;
90 default:
91 return type === "http"
92 ? tb.httpMetricsByRegionDaily
93 : tb.tcpMetricsByRegionDaily;
94 }
95}
96
97export function getMetricsByIntervalProcedure(period: Period, type: Type) {
98 switch (period) {
99 case "1d":
100 return type === "http"
101 ? tb.httpMetricsByIntervalDaily
102 : tb.tcpMetricsByIntervalDaily;
103 case "7d":
104 return type === "http"
105 ? tb.httpMetricsByIntervalWeekly
106 : tb.tcpMetricsByIntervalWeekly;
107 case "14d":
108 return type === "http"
109 ? tb.httpMetricsByIntervalBiweekly
110 : tb.tcpMetricsByIntervalBiweekly;
111 default:
112 return type === "http"
113 ? tb.httpMetricsByIntervalDaily
114 : tb.tcpMetricsByIntervalDaily;
115 }
116}
117
118// FIXME: tb pipes are deprecated, we need new ones
119export function getMetricsRegionsProcedure(period: Period, type: Type) {
120 switch (period) {
121 case "1d":
122 if (type === "dns") return tb.dnsMetricsRegionsBiweekly;
123 if (type === "http") return tb.httpMetricsRegionsDaily;
124 if (type === "tcp") return tb.tcpMetricsByIntervalDaily;
125 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
126 case "7d":
127 if (type === "dns") return tb.dnsMetricsRegionsBiweekly;
128 if (type === "http") return tb.httpMetricsRegionsWeekly;
129 if (type === "tcp") return tb.tcpMetricsByIntervalWeekly;
130 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
131 case "14d":
132 if (type === "dns") return tb.dnsMetricsRegionsBiweekly;
133 if (type === "http") return tb.httpMetricsRegionsBiweekly;
134 if (type === "tcp") return tb.tcpMetricsByIntervalBiweekly;
135 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
136 default:
137 if (type === "dns") return tb.dnsMetricsRegionsBiweekly;
138 if (type === "http") return tb.httpMetricsRegionsDaily;
139 if (type === "tcp") return tb.tcpMetricsByIntervalDaily;
140 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
141 }
142}
143
144export function getStatusProcedure(_period: "45d", type: Type) {
145 if (type === "dns") return tb.dnsStatus45d;
146 if (type === "http") return tb.httpStatus45d;
147 if (type === "tcp") return tb.tcpStatus45d;
148 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
149}
150
151export function getGetProcedure(period: "14d", type: Type) {
152 switch (period) {
153 case "14d":
154 if (type === "http") return tb.httpGetBiweekly;
155 if (type === "tcp") return tb.tcpGetBiweekly;
156 if (type === "dns") return tb.dnsGetBiweekly;
157 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
158 default:
159 if (type === "http") return tb.httpGetBiweekly;
160 if (type === "tcp") return tb.tcpGetBiweekly;
161 if (type === "dns") return tb.dnsGetBiweekly;
162 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
163 }
164}
165
166export function getGlobalMetricsProcedure(type: Type) {
167 return type === "http" ? tb.httpGlobalMetricsDaily : tb.tcpGlobalMetricsDaily;
168}
169
170export function getUptimeProcedure(period: "7d" | "30d", type: Type) {
171 switch (period) {
172 case "7d":
173 if (type === "dns") return tb.dnsUptime30d;
174 if (type === "http") return tb.httpUptimeWeekly;
175 if (type === "tcp") return tb.tcpUptimeWeekly;
176 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
177 case "30d":
178 if (type === "dns") return tb.dnsUptime30d;
179 if (type === "http") return tb.httpUptime30d;
180 if (type === "tcp") return tb.tcpUptime30d;
181 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
182 default:
183 if (type === "dns") return tb.dnsUptime30d;
184 if (type === "http") return tb.httpUptime30d;
185 if (type === "tcp") return tb.tcpUptime30d;
186 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
187 }
188}
189
190// TODO: missing pipes for other periods
191export function getMetricsLatencyProcedure(_period: Period, type: Type) {
192 switch (_period) {
193 case "1d":
194 if (type === "dns") return tb.dnsMetricsLatency7d;
195 if (type === "http") return tb.httpMetricsLatency1d;
196 if (type === "tcp") return tb.tcpMetricsLatency1d;
197 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
198 case "7d":
199 if (type === "dns") return tb.dnsMetricsLatency7d;
200 if (type === "http") return tb.httpMetricsLatency7d;
201 if (type === "tcp") return tb.tcpMetricsLatency7d;
202 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
203 default:
204 if (type === "dns") return tb.dnsMetricsLatency7d;
205 if (type === "http") return tb.httpMetricsLatency1d;
206 if (type === "tcp") return tb.tcpMetricsLatency1d;
207 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
208 }
209}
210
211export function getMetricsLatencyMultiProcedure(_period: Period, type: Type) {
212 if (type === "dns") return tb.dnsMetricsLatency1dMulti;
213 if (type === "http") return tb.httpMetricsLatency1dMulti;
214 if (type === "tcp") return tb.tcpMetricsLatency1dMulti;
215 throw new TRPCError({ code: "NOT_FOUND", message: "Invalid type" });
216}
217
218export function getTimingPhasesProcedure(type: Type) {
219 return type === "http" ? tb.httpTimingPhases14d : null;
220}
221
222export const tinybirdRouter = createTRPCRouter({
223 // Legacy procedure for backward compatibility
224 httpGetMonthly: protectedProcedure
225 .input(
226 z.object({
227 monitorId: z.string(),
228 region: z.enum(monitorRegions).or(z.string()).optional(),
229 cronTimestamp: z.int().optional(),
230 }),
231 )
232 .query(async (opts) => {
233 return await tb.httpGetMonthly(opts.input);
234 }),
235
236 // Simplified procedures that handle period/type logic on server
237 list: protectedProcedure
238 .input(
239 z.object({
240 monitorId: z.string(),
241 region: z.enum(monitorRegions).or(z.string()).optional(),
242 cronTimestamp: z.int().optional(),
243 from: z.coerce.date().optional(),
244 to: z.coerce.date().optional(),
245 }),
246 )
247 .query(async (opts) => {
248 const whereConditions: SQL[] = [
249 eq(monitor.id, Number.parseInt(opts.input.monitorId)),
250 eq(monitor.workspaceId, opts.ctx.workspace.id),
251 ];
252
253 const _monitor = await db.query.monitor.findFirst({
254 where: and(...whereConditions),
255 });
256
257 if (!_monitor) {
258 throw new TRPCError({
259 code: "NOT_FOUND",
260 message: "Monitor not found",
261 });
262 }
263
264 const period = calculatePeriod(opts.input.from, opts.input.to);
265
266 const procedure = getListProcedure(
267 period,
268 _monitor.jobType as "http" | "tcp" | "dns",
269 );
270 return await procedure({
271 ...opts.input,
272 fromDate: opts.input.from?.getTime() ?? undefined,
273 toDate: opts.input.to?.getTime(),
274 });
275 }),
276
277 uptime: protectedProcedure
278 .input(
279 z.object({
280 monitorId: z.string(),
281 fromDate: z.string().optional(),
282 toDate: z.string().optional(),
283 interval: z.int().optional(), // in minutes, default 30
284 regions: z.enum(monitorRegions).or(z.string()).array().optional(),
285 type: z.enum(types).prefault("http"),
286 period: z.enum(["7d", "30d"]).prefault("30d"),
287 }),
288 )
289 .query(async (opts) => {
290 const whereConditions: SQL[] = [
291 eq(monitor.id, Number.parseInt(opts.input.monitorId)),
292 eq(monitor.workspaceId, opts.ctx.workspace.id),
293 ];
294
295 const _monitor = await db.query.monitor.findFirst({
296 where: and(...whereConditions),
297 });
298
299 if (!_monitor) {
300 throw new TRPCError({
301 code: "NOT_FOUND",
302 message: "Monitor not found",
303 });
304 }
305
306 const procedure = getUptimeProcedure(opts.input.period, opts.input.type);
307 return await procedure(opts.input);
308 }),
309
310 auditLog: protectedProcedure
311 .input(
312 z.object({
313 monitorId: z.string(),
314 interval: z.int().prefault(30), // in days
315 }),
316 )
317 .query(async (opts) => {
318 const whereConditions: SQL[] = [
319 eq(monitor.id, Number.parseInt(opts.input.monitorId)),
320 eq(monitor.workspaceId, opts.ctx.workspace.id),
321 ];
322
323 const _monitor = await db.query.monitor.findFirst({
324 where: and(...whereConditions),
325 });
326
327 if (!_monitor) {
328 throw new TRPCError({
329 code: "NOT_FOUND",
330 message: "Monitor not found",
331 });
332 }
333
334 return await tb.getAuditLog({
335 monitorId: `monitor:${opts.input.monitorId}`,
336 interval: opts.input.interval,
337 });
338 }),
339
340 metrics: protectedProcedure
341 .input(
342 z.object({
343 monitorId: z.string(),
344 period: z.enum(periods),
345 type: z.enum(types).prefault("http"),
346 regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
347 cronTimestamp: z.int().optional(),
348 }),
349 )
350 .query(async (opts) => {
351 const whereConditions: SQL[] = [
352 eq(monitor.id, Number.parseInt(opts.input.monitorId)),
353 eq(monitor.workspaceId, opts.ctx.workspace.id),
354 ];
355
356 const _monitor = await db.query.monitor.findFirst({
357 where: and(...whereConditions),
358 });
359
360 if (opts.ctx.workspace.plan === "free") {
361 opts.input.regions = undefined;
362 }
363
364 if (!_monitor) {
365 throw new TRPCError({
366 code: "NOT_FOUND",
367 message: "Monitor not found",
368 });
369 }
370
371 const procedure = getMetricsProcedure(opts.input.period, opts.input.type);
372 return await procedure(opts.input);
373 }),
374
375 metricsByRegion: protectedProcedure
376 .input(
377 z.object({
378 monitorId: z.string(),
379 period: z.enum(periods),
380 type: z.enum(types).prefault("http"),
381 region: z.enum(monitorRegions).or(z.string()).optional(),
382 cronTimestamp: z.int().optional(),
383 }),
384 )
385 .query(async (opts) => {
386 const whereConditions: SQL[] = [
387 eq(monitor.id, Number.parseInt(opts.input.monitorId)),
388 eq(monitor.workspaceId, opts.ctx.workspace.id),
389 ];
390
391 const _monitor = await db.query.monitor.findFirst({
392 where: and(...whereConditions),
393 });
394
395 if (!_monitor) {
396 throw new TRPCError({
397 code: "NOT_FOUND",
398 message: "Monitor not found",
399 });
400 }
401
402 const procedure = getMetricsByRegionProcedure(
403 opts.input.period,
404 opts.input.type,
405 );
406 return await procedure(opts.input);
407 }),
408
409 metricsByInterval: protectedProcedure
410 .input(
411 z.object({
412 monitorId: z.string(),
413 period: z.enum(periods),
414 type: z.enum(types).prefault("http"),
415 region: z.enum(monitorRegions).or(z.string()).optional(),
416 cronTimestamp: z.int().optional(),
417 }),
418 )
419 .query(async (opts) => {
420 const whereConditions: SQL[] = [
421 eq(monitor.id, Number.parseInt(opts.input.monitorId)),
422 eq(monitor.workspaceId, opts.ctx.workspace.id),
423 ];
424
425 const _monitor = await db.query.monitor.findFirst({
426 where: and(...whereConditions),
427 });
428
429 if (!_monitor) {
430 throw new TRPCError({
431 code: "NOT_FOUND",
432 message: "Monitor not found",
433 });
434 }
435
436 const procedure = getMetricsByIntervalProcedure(
437 opts.input.period,
438 opts.input.type,
439 );
440 return await procedure(opts.input);
441 }),
442
443 metricsRegions: protectedProcedure
444 .input(
445 z.object({
446 monitorId: z.string(),
447 period: z.enum(periods),
448 type: z.enum(types).prefault("http"),
449 // Additional filters
450 interval: z.int().optional(),
451 regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
452 cronTimestamp: z.int().optional(),
453 fromDate: z.string().optional(),
454 toDate: z.string().optional(),
455 }),
456 )
457 .query(async (opts) => {
458 const whereConditions: SQL[] = [
459 eq(monitor.id, Number.parseInt(opts.input.monitorId)),
460 eq(monitor.workspaceId, opts.ctx.workspace.id),
461 ];
462
463 const _monitor = await db.query.monitor.findFirst({
464 where: and(...whereConditions),
465 });
466
467 if (!_monitor) {
468 throw new TRPCError({
469 code: "NOT_FOUND",
470 message: "Monitor not found",
471 });
472 }
473
474 if (opts.ctx.workspace.plan === "free") {
475 opts.input.regions = undefined;
476 }
477
478 const procedure = getMetricsRegionsProcedure(
479 opts.input.period,
480 opts.input.type,
481 );
482 return await procedure(opts.input);
483 }),
484
485 status: protectedProcedure
486 .input(
487 z.object({
488 monitorIds: z.string().array(),
489 period: z.enum(["45d"]),
490 type: z.enum(types).prefault("http"),
491 region: z.enum(monitorRegions).or(z.string()).optional(),
492 cronTimestamp: z.int().optional(),
493 }),
494 )
495 .query(async (opts) => {
496 const whereConditions: SQL[] = [
497 inArray(monitor.id, opts.input.monitorIds.map(Number)),
498 eq(monitor.workspaceId, opts.ctx.workspace.id),
499 ];
500
501 const _monitors = await db.query.monitor.findMany({
502 where: and(...whereConditions),
503 });
504
505 if (_monitors.length !== opts.input.monitorIds.length) {
506 throw new TRPCError({
507 code: "NOT_FOUND",
508 message: "Some monitors not found",
509 });
510 }
511
512 const procedure = getStatusProcedure(opts.input.period, opts.input.type);
513 return await procedure(opts.input);
514 }),
515
516 get: protectedProcedure
517 .input(
518 z.object({
519 id: z.string().nullable(),
520 monitorId: z.string(),
521 period: z.enum(["14d"]).prefault("14d"),
522 }),
523 )
524 .query(async (opts) => {
525 const whereConditions: SQL[] = [
526 eq(monitor.id, Number.parseInt(opts.input.monitorId)),
527 eq(monitor.workspaceId, opts.ctx.workspace.id),
528 ];
529
530 const _monitor = await db.query.monitor.findFirst({
531 where: and(...whereConditions),
532 });
533
534 if (!_monitor) {
535 throw new TRPCError({
536 code: "NOT_FOUND",
537 message: "Monitor not found",
538 });
539 }
540
541 const procedure = getGetProcedure(
542 opts.input.period,
543 _monitor.jobType as "http" | "tcp" | "dns",
544 );
545 return await procedure(opts.input);
546 }),
547
548 globalMetrics: protectedProcedure
549 .input(
550 z.object({
551 monitorIds: z.string().array(),
552 type: z.enum(types).prefault("http"),
553 }),
554 )
555 .query(async (opts) => {
556 const whereConditions: SQL[] = [
557 eq(monitor.workspaceId, opts.ctx.workspace.id),
558 inArray(monitor.id, opts.input.monitorIds.map(Number)),
559 ];
560
561 const _monitors = await db.query.monitor.findMany({
562 where: and(...whereConditions),
563 });
564
565 if (_monitors.length !== opts.input.monitorIds.length) {
566 throw new TRPCError({
567 code: "NOT_FOUND",
568 message: "Some monitors not found",
569 });
570 }
571
572 const procedure = getGlobalMetricsProcedure(opts.input.type);
573 return await procedure(opts.input);
574 }),
575
576 metricsLatency: protectedProcedure
577 .input(
578 z.object({
579 monitorId: z.string(),
580 period: z.enum(periods),
581 regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
582 type: z.enum(types).prefault("http"),
583 fromDate: z.string().optional(),
584 toDate: z.string().optional(),
585 }),
586 )
587 .query(async (opts) => {
588 if (opts.ctx.workspace.plan === "free") {
589 opts.input.regions = undefined;
590 }
591
592 const procedure = getMetricsLatencyProcedure(
593 opts.input.period,
594 opts.input.type,
595 );
596 return await procedure(opts.input);
597 }),
598
599 metricsTimingPhases: protectedProcedure
600 .input(
601 z.object({
602 monitorId: z.string(),
603 period: z.enum(periods),
604 interval: z.int().optional(),
605 regions: z.array(z.enum(monitorRegions).or(z.string())).optional(),
606 type: z.literal("http"),
607 }),
608 )
609 .query(async (opts) => {
610 if (opts.ctx.workspace.plan === "free") {
611 opts.input.regions = undefined;
612 }
613
614 const procedure = getTimingPhasesProcedure(opts.input.type);
615
616 if (!procedure) {
617 throw new TRPCError({
618 code: "NOT_FOUND",
619 message: "Timing phases not supported for this type",
620 });
621 }
622
623 return await procedure(opts.input);
624 }),
625
626 metricsLatencyMulti: protectedProcedure
627 .input(
628 z.object({
629 monitorIds: z.string().array(),
630 period: z.enum(["1d"]).prefault("1d"),
631 type: z.enum(types).prefault("http"),
632 }),
633 )
634 .query(async (opts) => {
635 const procedure = getMetricsLatencyMultiProcedure(
636 opts.input.period,
637 opts.input.type,
638 );
639 return await procedure(opts.input);
640 }),
641
642 workspace30d: protectedProcedure
643 .input(
644 z.object({
645 type: z.enum(types).prefault("http"),
646 }),
647 )
648 .query(async (opts) => {
649 const procedure = getWorkspace30dProcedure(opts.input.type);
650 return await procedure({ workspaceId: String(opts.ctx.workspace.id) });
651 }),
652});