Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update cli endpoint (#1073)

* 🔥 improve returning data

* 🚧 cli endpont

* ci: apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Thibault Le Ouay
autofix-ci[bot]
and committed by
GitHub
d6334213 30aa303d

+261 -39
+6 -1
apps/checker/handlers/checker.go
··· 88 88 } 89 89 90 90 var called int 91 + var result checker.Response 91 92 92 93 op := func() error { 93 94 called++ ··· 174 175 return fmt.Errorf("unable to ping: %v with status %v", res, res.Status) 175 176 } 176 177 178 + result = res 179 + result.Region = h.Region 180 + 177 181 // it's in error if not successful 178 182 if isSuccessfull { 179 183 data.Error = 0 ··· 181 185 data.Body = "" 182 186 } else { 183 187 data.Error = 1 188 + result.Error = "Error" 184 189 } 185 190 186 191 data.Assertions = assertionAsString ··· 302 307 303 308 } 304 309 305 - c.JSON(http.StatusOK, gin.H{"message": "ok"}) 310 + c.JSON(http.StatusOK, result) 306 311 }
+12 -1
apps/checker/handlers/tcp.go
··· 95 95 } 96 96 97 97 var called int 98 + var response TCPResponse 98 99 99 100 op := func() error { 100 101 called++ ··· 123 124 CronTimestamp: req.CronTimestamp, 124 125 Trigger: trigger, 125 126 URI: req.URL, 127 + } 128 + 129 + response = TCPResponse{ 130 + Timestamp: res.TCPStart, 131 + Timing: checker.TCPResponseTiming{ 132 + TCPStart: res.TCPStart, 133 + TCPDone: res.TCPDone, 134 + }, 135 + Latency: latency, 136 + Region: h.Region, 126 137 } 127 138 128 139 if req.Status == "active" && req.DegradedAfter > 0 && latency > req.DegradedAfter { ··· 196 207 } 197 208 } 198 209 199 - c.JSON(http.StatusOK, gin.H{"message": "ok"}) 210 + c.JSON(http.StatusOK, response) 200 211 } 201 212 202 213 func (h Handler) TCPHandlerRegion(c *gin.Context) {
+2 -37
apps/server/src/v1/monitors/results/get.ts
··· 9 9 import { env } from "../../../env"; 10 10 import { openApiErrorResponses } from "../../../libs/errors/openapi-error-responses"; 11 11 import type { monitorsApi } from "../index"; 12 - import { ParamsSchema } from "../schema"; 12 + import { ParamsSchema, ResultRun } from "../schema"; 13 13 14 14 const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 15 15 16 - const timingSchema = z.object({ 17 - dnsStart: z.number(), 18 - dnsDone: z.number(), 19 - connectStart: z.number(), 20 - connectDone: z.number(), 21 - tlsHandshakeStart: z.number(), 22 - tlsHandshakeDone: z.number(), 23 - firstByteStart: z.number(), 24 - firstByteDone: z.number(), 25 - transferStart: z.number(), 26 - transferDone: z.number(), 27 - }); 28 - 29 16 const getMonitorStats = createRoute({ 30 17 method: "get", 31 18 tags: ["monitor"], ··· 42 29 200: { 43 30 content: { 44 31 "application/json": { 45 - schema: z.array( 46 - z.object({ 47 - latency: z.number().int(), // in ms 48 - statusCode: z.number().int().nullable().default(null), 49 - monitorId: z.string().default(""), 50 - url: z.string().url().optional(), 51 - error: z 52 - .number() 53 - .default(0) 54 - .transform((val) => val !== 0), 55 - region: z.enum(flyRegions), 56 - timestamp: z.number().int().optional(), 57 - message: z.string().nullable().optional(), 58 - timing: z 59 - .preprocess((val) => { 60 - if (!val) return null; 61 - const value = timingSchema.safeParse(JSON.parse(String(val))); 62 - if (value.success) return value.data; 63 - return null; 64 - }, timingSchema.nullable()) 65 - .optional(), 66 - }), 67 - ), 32 + schema: z.array(ResultRun), 68 33 }, 69 34 }, 70 35 description: "All the metrics for the monitor",
+196
apps/server/src/v1/monitors/run/post.ts
··· 1 + import { createRoute, z } from "@hono/zod-openapi"; 2 + import { and, eq, gte, isNull, sql } from "@openstatus/db"; 3 + import { db } from "@openstatus/db/src/db"; 4 + import { monitorRun } from "@openstatus/db/src/schema"; 5 + import { monitorStatusTable } from "@openstatus/db/src/schema/monitor_status/monitor_status"; 6 + import { selectMonitorStatusSchema } from "@openstatus/db/src/schema/monitor_status/validation"; 7 + import { monitor } from "@openstatus/db/src/schema/monitors/monitor"; 8 + import { selectMonitorSchema } from "@openstatus/db/src/schema/monitors/validation"; 9 + import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 10 + import type { httpPayloadSchema, tpcPayloadSchema } from "@openstatus/utils"; 11 + import { HTTPException } from "hono/http-exception"; 12 + import type { monitorsApi } from ".."; 13 + import { env } from "../../../env"; 14 + import { openApiErrorResponses } from "../../../libs/errors/openapi-error-responses"; 15 + import { HTTPTriggerResult, ParamsSchema } from "../schema"; 16 + 17 + const triggerMonitor = createRoute({ 18 + method: "post", 19 + tags: ["monitor"], 20 + description: "Run a monitor check", 21 + path: "/:id/run", 22 + request: { 23 + params: ParamsSchema, 24 + }, 25 + responses: { 26 + 200: { 27 + content: { 28 + "application/json": { 29 + schema: z.array(HTTPTriggerResult), 30 + }, 31 + }, 32 + description: "All the historical metrics", 33 + }, 34 + ...openApiErrorResponses, 35 + }, 36 + }); 37 + 38 + export function registerTriggerMonitor(api: typeof monitorsApi) { 39 + return api.openapi(triggerMonitor, async (c) => { 40 + const workspaceId = c.get("workspaceId"); 41 + const { id } = c.req.valid("param"); 42 + const limits = c.get("limits"); 43 + 44 + const lastMonth = new Date().setMonth(new Date().getMonth() - 1); 45 + 46 + const count = ( 47 + await db 48 + .select({ count: sql<number>`count(*)` }) 49 + .from(monitorRun) 50 + .where( 51 + and( 52 + eq(monitorRun.workspaceId, Number(workspaceId)), 53 + gte(monitorRun.createdAt, new Date(lastMonth)), 54 + ), 55 + ) 56 + .all() 57 + )[0].count; 58 + 59 + if (count >= getLimit(limits, "synthetic-checks")) { 60 + throw new HTTPException(403, { 61 + message: "Upgrade for more checks", 62 + }); 63 + } 64 + 65 + const monitorData = await db 66 + .select() 67 + .from(monitor) 68 + .where( 69 + and( 70 + eq(monitor.id, Number(id)), 71 + eq(monitor.workspaceId, Number(workspaceId)), 72 + isNull(monitor.deletedAt), 73 + ), 74 + ) 75 + .get(); 76 + 77 + if (!monitorData) { 78 + throw new HTTPException(404, { message: "Not Found" }); 79 + } 80 + 81 + const parseMonitor = selectMonitorSchema.safeParse(monitorData); 82 + 83 + if (!parseMonitor.success) { 84 + throw new HTTPException(400, { message: "Something went wrong" }); 85 + } 86 + 87 + const row = parseMonitor.data; 88 + 89 + // Maybe later overwrite the region 90 + 91 + const monitorStatusData = await db 92 + .select() 93 + .from(monitorStatusTable) 94 + .where(eq(monitorStatusTable.monitorId, monitorData.id)) 95 + .all(); 96 + 97 + const monitorStatus = z 98 + .array(selectMonitorStatusSchema) 99 + .safeParse(monitorStatusData); 100 + if (!monitorStatus.success) { 101 + throw new HTTPException(400, { message: "Something went wrong" }); 102 + } 103 + 104 + const timestamp = Date.now(); 105 + 106 + const newRun = await db 107 + .insert(monitorRun) 108 + .values({ 109 + monitorId: row.id, 110 + workspaceId: row.workspaceId, 111 + runnedAt: new Date(timestamp), 112 + }) 113 + .returning(); 114 + 115 + if (!newRun[0]) { 116 + throw new HTTPException(400, { message: "Something went wrong" }); 117 + } 118 + 119 + const allResult = []; 120 + for (const region of parseMonitor.data.regions) { 121 + const status = 122 + monitorStatus.data.find((m) => region === m.region)?.status || "active"; 123 + // Trigger the monitor 124 + 125 + let payload: 126 + | z.infer<typeof httpPayloadSchema> 127 + | z.infer<typeof tpcPayloadSchema> 128 + | null = null; 129 + // 130 + if (row.jobType === "http") { 131 + payload = { 132 + workspaceId: String(row.workspaceId), 133 + monitorId: String(row.id), 134 + url: row.url, 135 + method: row.method || "GET", 136 + cronTimestamp: timestamp, 137 + body: row.body, 138 + headers: row.headers, 139 + status: status, 140 + assertions: row.assertions ? JSON.parse(row.assertions) : null, 141 + degradedAfter: row.degradedAfter, 142 + timeout: row.timeout, 143 + trigger: "api", 144 + }; 145 + } 146 + if (row.jobType === "tcp") { 147 + payload = { 148 + workspaceId: String(row.workspaceId), 149 + monitorId: String(row.id), 150 + uri: row.url, 151 + status: status, 152 + assertions: row.assertions ? JSON.parse(row.assertions) : null, 153 + cronTimestamp: timestamp, 154 + degradedAfter: row.degradedAfter, 155 + timeout: row.timeout, 156 + trigger: "api", 157 + }; 158 + } 159 + 160 + if (!payload) { 161 + throw new Error("Invalid jobType"); 162 + } 163 + const url = generateUrl({ row }); 164 + const result = fetch(url, { 165 + headers: { 166 + "Content-Type": "application/json", 167 + "fly-prefer-region": region, // Specify the region you want the request to be sent to 168 + Authorization: `Basic ${env.CRON_SECRET}`, 169 + }, 170 + method: "POST", 171 + body: JSON.stringify(payload), 172 + }); 173 + allResult.push(result); 174 + } 175 + 176 + const result = await Promise.all(allResult); 177 + const data = z.array(HTTPTriggerResult).safeParse(result); 178 + 179 + if (!data.success) { 180 + throw new HTTPException(400, { message: "Something went wrong" }); 181 + } 182 + 183 + return c.json(data.data, 200); 184 + }); 185 + } 186 + 187 + function generateUrl({ row }: { row: z.infer<typeof selectMonitorSchema> }) { 188 + switch (row.jobType) { 189 + case "http": 190 + return `https://openstatus-checker.fly.dev/checker/http?monitor_id=${row.id}&trigger=api`; 191 + case "tcp": 192 + return `https://openstatus-checker.fly.dev/checker/tcp?monitor_id=${row.id}&trigger=api`; 193 + default: 194 + throw new Error("Invalid jobType"); 195 + } 196 + }
+45
apps/server/src/v1/monitors/schema.ts
··· 219 219 }); 220 220 221 221 export type MonitorSchema = z.infer<typeof MonitorSchema>; 222 + 223 + const timingSchema = z.object({ 224 + dnsStart: z.number(), 225 + dnsDone: z.number(), 226 + connectStart: z.number(), 227 + connectDone: z.number(), 228 + tlsHandshakeStart: z.number(), 229 + tlsHandshakeDone: z.number(), 230 + firstByteStart: z.number(), 231 + firstByteDone: z.number(), 232 + transferStart: z.number(), 233 + transferDone: z.number(), 234 + }); 235 + 236 + export const HTTPTriggerResult = z.object({ 237 + status: z.number(), 238 + latency: z.number(), 239 + headers: z.record(z.string()), 240 + timestamp: z.number(), 241 + timing: timingSchema, 242 + body: z.string().optional().nullable(), 243 + error: z.string().optional().nullable(), 244 + }); 245 + 246 + export const ResultRun = z.object({ 247 + latency: z.number().int(), // in ms 248 + statusCode: z.number().int().nullable().default(null), 249 + monitorId: z.string().default(""), 250 + url: z.string().url().optional(), 251 + error: z 252 + .number() 253 + .default(0) 254 + .transform((val) => val !== 0), 255 + region: z.enum(flyRegions), 256 + timestamp: z.number().int().optional(), 257 + message: z.string().nullable().optional(), 258 + timing: z 259 + .preprocess((val) => { 260 + if (!val) return null; 261 + const value = timingSchema.safeParse(JSON.parse(String(val))); 262 + if (value.success) return value.data; 263 + return null; 264 + }, timingSchema.nullable()) 265 + .optional(), 266 + });