Openstatus www.openstatus.dev

🔥 to the moon (#494)

authored by

Thibault Le Ouay and committed by
GitHub
a4a53986 82209304

+266 -26
+21
apps/checker/Dockerfile
··· 1 + FROM golang:1.21 AS builder 2 + 3 + WORKDIR /go/src/github.com/openstatushq/openstatus/apps/checker 4 + 5 + COPY go.mod . 6 + COPY go.sum . 7 + 8 + RUN go mod download 9 + 10 + COPY . . 11 + 12 + ARG VERSION 13 + 14 + RUN go build -o openstatus-checker . 15 + 16 + FROM golang:1.21 17 + 18 + COPY --from=builder /go/src/github.com/openstatushq/openstatus/apps/checker/openstatus-checker /usr/local/bin/openstatus-checker 19 + 20 + 21 + CMD ["/usr/local/bin/openstatus-checker"]
+30
apps/checker/fly.toml
··· 1 + # fly.toml app configuration file generated for openstatus-checker on 2023-11-30T20:23:20+01:00 2 + # 3 + # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 + # 5 + 6 + app = "openstatus-checker" 7 + primary_region = "ams" 8 + 9 + [build] 10 + dockerfile = "./Dockerfile" 11 + 12 + [deploy] 13 + strategy = "canary" 14 + 15 + 16 + [env] 17 + PORT = "8080" 18 + 19 + [http_service] 20 + internal_port = 8080 21 + force_https = true 22 + auto_stop_machines = true 23 + auto_start_machines = true 24 + min_machines_running = 0 25 + processes = ["app"] 26 + 27 + [[vm]] 28 + cpu_kind = "shared" 29 + cpus = 2 30 + memory_mb = 512
+8
apps/checker/go.mod
··· 1 + module github.com/openstatushq/openstatus/apps/checker 2 + 3 + go 1.21.4 4 + 5 + require ( 6 + github.com/go-chi/chi/v5 v5.0.10 7 + github.com/google/uuid v1.4.0 8 + )
+4
apps/checker/go.sum
··· 1 + github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= 2 + github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 3 + github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 4 + github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+124
apps/checker/main.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "encoding/json" 6 + "fmt" 7 + "net/http" 8 + "os" 9 + "time" 10 + 11 + "github.com/go-chi/chi/v5" 12 + "github.com/go-chi/chi/v5/middleware" 13 + ) 14 + 15 + type InputData struct { 16 + WorkspaceId string `json:"workspaceId"` 17 + Url string `json:"url"` 18 + MonitorId string `json:"monitorId"` 19 + Method string `json:"method"` 20 + CronTimestamp int64 `json:"cronTimestamp"` 21 + Body string `json:"body"` 22 + Headers []struct { 23 + Key string `json:"key"` 24 + Value string `json:"value"` 25 + } `json:"headers,omitempty"` 26 + PagesIds []string `json:"pagesIds"` 27 + Status string `json:"status"` 28 + } 29 + 30 + func main() { 31 + r := chi.NewRouter() 32 + r.Use(middleware.Logger) 33 + r.Post("/", func(w http.ResponseWriter, r *http.Request) { 34 + if r.Header.Get("Authorization") != "Basic "+ os.Getenv("CRON_SECRET") { 35 + http.Error(w, "Unauthorized", 401) 36 + return 37 + } 38 + region := os.Getenv("FLY_REGION") 39 + if r.Body == nil { 40 + http.Error(w, "Please send a request body", 400) 41 + return 42 + } 43 + var u InputData 44 + 45 + err := json.NewDecoder(r.Body).Decode(&u) 46 + 47 + fmt.Printf("Start checker for %+v", u) 48 + 49 + if err != nil { 50 + http.Error(w, err.Error(), 400) 51 + return 52 + } 53 + request, error := http.NewRequest(u.Method, u.Url, bytes.NewReader([]byte(u.Body))) 54 + 55 + // Setting headers 56 + for _, header := range u.Headers { 57 + fmt.Printf("%+v", header) 58 + if header.Key != "" && header.Value != "" { 59 + request.Header.Set(header.Key, header.Value) 60 + } 61 + } 62 + 63 + if error != nil { 64 + fmt.Println(error) 65 + } 66 + 67 + client := &http.Client{} 68 + start := time.Now().UTC().UnixMilli() 69 + response, error := client.Do(request) 70 + end := time.Now().UTC().UnixMilli() 71 + 72 + // Retry if error 73 + if error != nil { 74 + response, error = client.Do(request) 75 + end = time.Now().UTC().UnixMilli() 76 + } 77 + 78 + latency := end - start 79 + fmt.Println("🚀 Checked url: %v with latency %v in region %v ", u.Url, latency, region) 80 + fmt.Printf("Response %+v for %+v", response, u) 81 + if error != nil { 82 + tiny((PingData{ 83 + Latency: (latency), 84 + MonitorId: u.MonitorId, 85 + Region: region, 86 + WorkspaceId: u.WorkspaceId, 87 + Timestamp: time.Now().UTC().UnixMilli(), 88 + Url: u.Url, 89 + Message: error.Error(), 90 + })) 91 + } else { 92 + tiny((PingData{ 93 + Latency: (latency), 94 + MonitorId: u.MonitorId, 95 + Region: region, 96 + WorkspaceId: u.WorkspaceId, 97 + StatusCode: int16(response.StatusCode), 98 + Timestamp: time.Now().UTC().UnixMilli(), 99 + Url: u.Url, 100 + })) 101 + } 102 + 103 + fmt.Printf("End checker for %+v", u) 104 + 105 + w.Write([]byte("Ok")) 106 + w.WriteHeader(200) 107 + }) 108 + 109 + r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { 110 + data := struct { 111 + Ping string `json:"ping"` 112 + FlyRegion string `json:"fly_region"` 113 + }{ 114 + Ping: "pong", 115 + FlyRegion: os.Getenv("FLY_REGION"), 116 + } 117 + 118 + w.Header().Set("Content-Type", "application/json") 119 + w.WriteHeader(http.StatusCreated) 120 + json.NewEncoder(w).Encode(data) 121 + }) 122 + 123 + http.ListenAndServe(":8080", r) 124 + }
+45
apps/checker/ping.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "encoding/json" 6 + "fmt" 7 + "io" 8 + "net/http" 9 + "os" 10 + "time" 11 + ) 12 + 13 + type PingData struct { 14 + WorkspaceId string `json:"workspaceId"` 15 + MonitorId string `json:"monitorId"` 16 + Timestamp int64 `json:"timestamp"` 17 + StatusCode int16 `json:"statusCode"` 18 + Latency int64 `json:"latency"` 19 + CronTimestamp int64 `json:"cronTimestamp"` 20 + Url string `json:"url"` 21 + Region string `json:"region"` 22 + Message string `json:"message"` 23 + } 24 + 25 + func tiny(pingData PingData) { 26 + url := "https://api.tinybird.co/v0/events?name=golang_ping_response__v1" 27 + fmt.Println("URL:>", url) 28 + bearer := "Bearer " + os.Getenv("TINYBIRD_TOKEN") 29 + payloadBuf := new(bytes.Buffer) 30 + json.NewEncoder(payloadBuf).Encode(pingData) 31 + req, err := http.NewRequest("POST", url, payloadBuf) 32 + req.Header.Set("Authorization", bearer) 33 + req.Header.Set("Content-Type", "application/json") 34 + 35 + client := &http.Client{Timeout: time.Second * 10} 36 + resp, err := client.Do(req) 37 + if err != nil { 38 + fmt.Println(err) 39 + 40 + } 41 + defer resp.Body.Close() 42 + 43 + body, _ := io.ReadAll(resp.Body) 44 + fmt.Println(string(body)) 45 + }
+17 -6
apps/server/src/checker/checker.ts
··· 1 1 import { handleMonitorFailed, handleMonitorRecovered } from "./monitor-handler"; 2 2 import type { PublishPingType } from "./ping"; 3 - import { pingEndpoint, publishPing } from "./ping"; 3 + import { getHeaders, publishPing } from "./ping"; 4 4 import type { Payload } from "./schema"; 5 5 6 6 // we could have a 'retry' parameter to know how often we should retry ··· 54 54 let message = undefined; 55 55 // We are doing these for wrong urls 56 56 try { 57 - startTime = Date.now(); 58 - res = await pingEndpoint(data); 59 - endTime = Date.now(); 57 + const headers = getHeaders(data); 58 + console.log(`🆕 fetch is about to start for ${JSON.stringify(data)}`); 59 + startTime = performance.now(); 60 + res = await fetch(data.url, { 61 + method: data.method, 62 + keepalive: false, 63 + cache: "no-store", 64 + headers, 65 + // Avoid having "TypeError: Request with a GET or HEAD method cannot have a body." error 66 + ...(data.method === "POST" && { body: data?.body }), 67 + }); 68 + 69 + endTime = performance.now(); 70 + console.log(`✅ fetch is done for ${JSON.stringify(data)}`); 60 71 } catch (e) { 61 - endTime = Date.now(); 72 + endTime = performance.now(); 62 73 message = `${e}`; 63 74 console.log( 64 75 `🚨 error on pingEndpoint for ${JSON.stringify(data)} error: `, ··· 66 77 ); 67 78 } 68 79 69 - const latency = endTime - startTime; 80 + const latency = Number((endTime - startTime).toFixed(0)); 70 81 if (res?.ok) { 71 82 await publishPingRetryPolicy({ 72 83 payload: data,
+1 -18
apps/server/src/checker/ping.ts
··· 6 6 7 7 const region = env.FLY_REGION; 8 8 9 - function getHeaders(data?: Payload) { 9 + export function getHeaders(data?: Payload) { 10 10 const customHeaders = 11 11 data?.headers?.reduce((o, v) => { 12 12 // removes empty keys from the header ··· 17 17 "OpenStatus-Ping": "true", 18 18 ...customHeaders, 19 19 }; 20 - } 21 - 22 - export async function pingEndpoint(data: Payload) { 23 - try { 24 - const res = await fetch(data?.url, { 25 - method: data?.method, 26 - keepalive: false, 27 - cache: "no-store", 28 - headers: getHeaders(data), 29 - // Avoid having "TypeError: Request with a GET or HEAD method cannot have a body." error 30 - ...(data.method === "POST" && { body: data?.body }), 31 - }); 32 - 33 - return res; 34 - } catch (e) { 35 - throw e; 36 - } 37 20 } 38 21 39 22 export type PublishPingType = {
+16 -2
apps/web/src/app/api/checker/cron/_cron.ts
··· 92 92 body: Buffer.from(JSON.stringify(payload)).toString("base64"), 93 93 }, 94 94 }; 95 + const newTask: google.cloud.tasks.v2beta3.ITask = { 96 + httpRequest: { 97 + headers: { 98 + "Content-Type": "application/json", // Set content type to ensure compatibility your application's request parsing 99 + ...(region !== "auto" && { "fly-prefer-region": region }), // Specify the region you want the request to be sent to 100 + Authorization: `Basic ${env.CRON_SECRET}`, 101 + }, 102 + httpMethod: "POST", 103 + url: "https://openstatus-checker.fly.dev", 104 + body: Buffer.from(JSON.stringify(payload)).toString("base64"), 105 + }, 106 + }; 107 + 95 108 const request = { parent: parent, task: task }; 96 109 const [response] = await client.createTask(request); 97 - 98 - allResult.push(response); 110 + const requestNew = { parent: parent, task: newTask }; 111 + const [responseNew] = await client.createTask(requestNew); 112 + allResult.push(response, responseNew); 99 113 } 100 114 } 101 115 await Promise.all(allResult);