Openstatus www.openstatus.dev
at main 652 lines 21 kB view raw
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});