diff --git a/apps/checker/Dockerfile b/apps/checker/Dockerfile new file mode 100644 index 0000000000..87d51c4fdb --- /dev/null +++ b/apps/checker/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.21 AS builder + +WORKDIR /go/src/github.com/openstatushq/openstatus/apps/checker + +COPY go.mod . +COPY go.sum . + +RUN go mod download + +COPY . . + +ARG VERSION + +RUN go build -o openstatus-checker . + +FROM golang:1.21 + +COPY --from=builder /go/src/github.com/openstatushq/openstatus/apps/checker/openstatus-checker /usr/local/bin/openstatus-checker + + +CMD ["/usr/local/bin/openstatus-checker"] \ No newline at end of file diff --git a/apps/checker/fly.toml b/apps/checker/fly.toml new file mode 100644 index 0000000000..0de4169ddb --- /dev/null +++ b/apps/checker/fly.toml @@ -0,0 +1,30 @@ +# fly.toml app configuration file generated for openstatus-checker on 2023-11-30T20:23:20+01:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = "openstatus-checker" +primary_region = "ams" + +[build] + dockerfile = "./Dockerfile" + +[deploy] + strategy = "canary" + + +[env] + PORT = "8080" + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + processes = ["app"] + +[[vm]] + cpu_kind = "shared" + cpus = 2 + memory_mb = 512 diff --git a/apps/checker/go.mod b/apps/checker/go.mod new file mode 100644 index 0000000000..ab29bb2502 --- /dev/null +++ b/apps/checker/go.mod @@ -0,0 +1,8 @@ +module github.com/openstatushq/openstatus/apps/checker + +go 1.21.4 + +require ( + github.com/go-chi/chi/v5 v5.0.10 + github.com/google/uuid v1.4.0 +) diff --git a/apps/checker/go.sum b/apps/checker/go.sum new file mode 100644 index 0000000000..3bd92d34c8 --- /dev/null +++ b/apps/checker/go.sum @@ -0,0 +1,4 @@ +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/apps/checker/main.go b/apps/checker/main.go new file mode 100644 index 0000000000..86f74c20a8 --- /dev/null +++ b/apps/checker/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +type InputData struct { + WorkspaceId string `json:"workspaceId"` + Url string `json:"url"` + MonitorId string `json:"monitorId"` + Method string `json:"method"` + CronTimestamp int64 `json:"cronTimestamp"` + Body string `json:"body"` + Headers []struct { + Key string `json:"key"` + Value string `json:"value"` + } `json:"headers,omitempty"` + PagesIds []string `json:"pagesIds"` + Status string `json:"status"` +} + +func main() { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Post("/", func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Basic "+ os.Getenv("CRON_SECRET") { + http.Error(w, "Unauthorized", 401) + return + } + region := os.Getenv("FLY_REGION") + if r.Body == nil { + http.Error(w, "Please send a request body", 400) + return + } + var u InputData + + err := json.NewDecoder(r.Body).Decode(&u) + + fmt.Printf("Start checker for %+v", u) + + if err != nil { + http.Error(w, err.Error(), 400) + return + } + request, error := http.NewRequest(u.Method, u.Url, bytes.NewReader([]byte(u.Body))) + + // Setting headers + for _, header := range u.Headers { + fmt.Printf("%+v", header) + if header.Key != "" && header.Value != "" { + request.Header.Set(header.Key, header.Value) + } + } + + if error != nil { + fmt.Println(error) + } + + client := &http.Client{} + start := time.Now().UTC().UnixMilli() + response, error := client.Do(request) + end := time.Now().UTC().UnixMilli() + + // Retry if error + if error != nil { + response, error = client.Do(request) + end = time.Now().UTC().UnixMilli() + } + + latency := end - start + fmt.Println("🚀 Checked url: %v with latency %v in region %v ", u.Url, latency, region) + fmt.Printf("Response %+v for %+v", response, u) + if error != nil { + tiny((PingData{ + Latency: (latency), + MonitorId: u.MonitorId, + Region: region, + WorkspaceId: u.WorkspaceId, + Timestamp: time.Now().UTC().UnixMilli(), + Url: u.Url, + Message: error.Error(), + })) + } else { + tiny((PingData{ + Latency: (latency), + MonitorId: u.MonitorId, + Region: region, + WorkspaceId: u.WorkspaceId, + StatusCode: int16(response.StatusCode), + Timestamp: time.Now().UTC().UnixMilli(), + Url: u.Url, + })) + } + + fmt.Printf("End checker for %+v", u) + + w.Write([]byte("Ok")) + w.WriteHeader(200) + }) + + r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { + data := struct { + Ping string `json:"ping"` + FlyRegion string `json:"fly_region"` + }{ + Ping: "pong", + FlyRegion: os.Getenv("FLY_REGION"), + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(data) + }) + + http.ListenAndServe(":8080", r) +} diff --git a/apps/checker/ping.go b/apps/checker/ping.go new file mode 100644 index 0000000000..0b4902dc98 --- /dev/null +++ b/apps/checker/ping.go @@ -0,0 +1,45 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" +) + +type PingData struct { + WorkspaceId string `json:"workspaceId"` + MonitorId string `json:"monitorId"` + Timestamp int64 `json:"timestamp"` + StatusCode int16 `json:"statusCode"` + Latency int64 `json:"latency"` + CronTimestamp int64 `json:"cronTimestamp"` + Url string `json:"url"` + Region string `json:"region"` + Message string `json:"message"` +} + +func tiny(pingData PingData) { + url := "https://api.tinybird.co/v0/events?name=golang_ping_response__v1" + fmt.Println("URL:>", url) + bearer := "Bearer " + os.Getenv("TINYBIRD_TOKEN") + payloadBuf := new(bytes.Buffer) + json.NewEncoder(payloadBuf).Encode(pingData) + req, err := http.NewRequest("POST", url, payloadBuf) + req.Header.Set("Authorization", bearer) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: time.Second * 10} + resp, err := client.Do(req) + if err != nil { + fmt.Println(err) + + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + fmt.Println(string(body)) +} diff --git a/apps/server/src/checker/checker.ts b/apps/server/src/checker/checker.ts index 9cc40df12d..151116208d 100644 --- a/apps/server/src/checker/checker.ts +++ b/apps/server/src/checker/checker.ts @@ -1,6 +1,6 @@ import { handleMonitorFailed, handleMonitorRecovered } from "./monitor-handler"; import type { PublishPingType } from "./ping"; -import { pingEndpoint, publishPing } from "./ping"; +import { getHeaders, publishPing } from "./ping"; import type { Payload } from "./schema"; // we could have a 'retry' parameter to know how often we should retry @@ -54,11 +54,22 @@ const run = async (data: Payload, retry: number) => { let message = undefined; // We are doing these for wrong urls try { - startTime = Date.now(); - res = await pingEndpoint(data); - endTime = Date.now(); + const headers = getHeaders(data); + console.log(`🆕 fetch is about to start for ${JSON.stringify(data)}`); + startTime = performance.now(); + res = await fetch(data.url, { + method: data.method, + keepalive: false, + cache: "no-store", + headers, + // Avoid having "TypeError: Request with a GET or HEAD method cannot have a body." error + ...(data.method === "POST" && { body: data?.body }), + }); + + endTime = performance.now(); + console.log(`✅ fetch is done for ${JSON.stringify(data)}`); } catch (e) { - endTime = Date.now(); + endTime = performance.now(); message = `${e}`; console.log( `🚨 error on pingEndpoint for ${JSON.stringify(data)} error: `, @@ -66,7 +77,7 @@ const run = async (data: Payload, retry: number) => { ); } - const latency = endTime - startTime; + const latency = Number((endTime - startTime).toFixed(0)); if (res?.ok) { await publishPingRetryPolicy({ payload: data, diff --git a/apps/server/src/checker/ping.ts b/apps/server/src/checker/ping.ts index 84559ee221..cee9b35dec 100644 --- a/apps/server/src/checker/ping.ts +++ b/apps/server/src/checker/ping.ts @@ -6,7 +6,7 @@ import type { Payload } from "./schema"; const region = env.FLY_REGION; -function getHeaders(data?: Payload) { +export function getHeaders(data?: Payload) { const customHeaders = data?.headers?.reduce((o, v) => { // removes empty keys from the header @@ -19,23 +19,6 @@ function getHeaders(data?: Payload) { }; } -export async function pingEndpoint(data: Payload) { - try { - const res = await fetch(data?.url, { - method: data?.method, - keepalive: false, - cache: "no-store", - headers: getHeaders(data), - // Avoid having "TypeError: Request with a GET or HEAD method cannot have a body." error - ...(data.method === "POST" && { body: data?.body }), - }); - - return res; - } catch (e) { - throw e; - } -} - export type PublishPingType = { payload: Payload; latency: number; diff --git a/apps/web/src/app/api/checker/cron/_cron.ts b/apps/web/src/app/api/checker/cron/_cron.ts index 801b4e4f48..ebe0b32feb 100644 --- a/apps/web/src/app/api/checker/cron/_cron.ts +++ b/apps/web/src/app/api/checker/cron/_cron.ts @@ -92,10 +92,24 @@ export const cron = async ({ body: Buffer.from(JSON.stringify(payload)).toString("base64"), }, }; + const newTask: google.cloud.tasks.v2beta3.ITask = { + httpRequest: { + headers: { + "Content-Type": "application/json", // Set content type to ensure compatibility your application's request parsing + ...(region !== "auto" && { "fly-prefer-region": region }), // Specify the region you want the request to be sent to + Authorization: `Basic ${env.CRON_SECRET}`, + }, + httpMethod: "POST", + url: "https://openstatus-checker.fly.dev", + body: Buffer.from(JSON.stringify(payload)).toString("base64"), + }, + }; + const request = { parent: parent, task: task }; const [response] = await client.createTask(request); - - allResult.push(response); + const requestNew = { parent: parent, task: newTask }; + const [responseNew] = await client.createTask(requestNew); + allResult.push(response, responseNew); } } await Promise.all(allResult);