Openstatus
www.openstatus.dev
1package handlers
2
3import (
4 "encoding/json"
5 "fmt"
6 "net/http"
7 "time"
8
9 "github.com/cenkalti/backoff/v4"
10 "github.com/gin-gonic/gin"
11 "github.com/openstatushq/openstatus/apps/checker/checker"
12 "github.com/openstatushq/openstatus/apps/checker/request"
13 "github.com/rs/zerolog/log"
14)
15
16type PingResponse struct {
17 Body string `json:"body,omitempty"`
18 Headers string `json:"headers,omitempty"`
19 Region string `json:"region"`
20 Timing string `json:"timing,omitempty"`
21 RequestId int64 `json:"requestId,omitempty"`
22 WorkspaceId int64 `json:"workspaceId,omitempty"`
23 Latency int64 `json:"latency"`
24 Timestamp int64 `json:"timestamp"`
25 StatusCode int `json:"statusCode,omitempty"`
26}
27
28type Response struct {
29 Headers map[string]string `json:"headers,omitempty"`
30 Error string `json:"error,omitempty"`
31 Body string `json:"body,omitempty"`
32 Region string `json:"region"`
33 Tags []string `json:"tags,omitempty"`
34 RequestId int64 `json:"requestId,omitempty"`
35 WorkspaceId int64 `json:"workspaceId,omitempty"`
36 Latency int64 `json:"latency"`
37 Timestamp int64 `json:"timestamp"`
38 Timing checker.Timing `json:"timing"`
39 Status int `json:"status,omitempty"`
40}
41
42func (h Handler) PingRegionHandler(c *gin.Context) {
43 ctx := c.Request.Context()
44
45 dataSourceName := "check_response_http__v0"
46 region := c.Param("region")
47
48 if region == "" {
49 c.String(http.StatusBadRequest, "region is required")
50
51 return
52 }
53
54 fmt.Printf("Start of /ping/%s\n", region)
55
56 if c.GetHeader("Authorization") != fmt.Sprintf("Basic %s", h.Secret) {
57 c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
58
59 return
60 }
61
62 if h.CloudProvider == "fly" {
63 if region != h.Region {
64 c.Header("fly-replay", fmt.Sprintf("region=%s", region))
65 c.String(http.StatusAccepted, "Forwarding request to %s", region)
66
67 return
68 }
69 }
70
71 // We need a new client for each request to avoid connection reuse.
72 requestClient := &http.Client{
73 Timeout: 45 * time.Second,
74 }
75
76 defer requestClient.CloseIdleConnections()
77
78 var req request.PingRequest
79 if err := c.ShouldBindJSON(&req); err != nil {
80 log.Ctx(ctx).Error().Err(err).Msg("failed to decode checker request")
81 c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
82
83 return
84 }
85
86 var res checker.Response
87
88 op := func() error {
89
90 headers := make([]struct {
91 Key string `json:"key"`
92 Value string `json:"value"`
93 }, 0)
94
95 for key, value := range req.Headers {
96 headers = append(headers, struct {
97 Key string `json:"key"`
98 Value string `json:"value"`
99 }{Key: key, Value: value})
100 }
101
102 input := request.HttpCheckerRequest{
103 Headers: headers,
104 URL: req.URL,
105 Method: req.Method,
106 Body: req.Body,
107 }
108
109 r, err := checker.Http(c.Request.Context(), requestClient, input)
110
111 if err != nil {
112 return fmt.Errorf("unable to ping: %w", err)
113 }
114
115 timingAsString, err := json.Marshal(r.Timing)
116 if err != nil {
117 return fmt.Errorf("error while parsing timing data %s: %w", req.URL, err)
118 }
119
120 headersAsString, err := json.Marshal(r.Headers)
121 if err != nil {
122 return nil
123 }
124
125 tbData := PingResponse{
126 RequestId: req.RequestId,
127 WorkspaceId: req.WorkspaceId,
128 StatusCode: r.Status,
129 Latency: r.Latency,
130 Body: r.Body,
131 Headers: string(headersAsString),
132 Timestamp: r.Timestamp,
133 Timing: string(timingAsString),
134 Region: h.Region,
135 }
136
137 res = r
138 res.Region = h.Region
139
140 if tbData.RequestId != 0 {
141 if err := h.TbClient.SendEvent(ctx, tbData, dataSourceName); err != nil {
142 log.Ctx(ctx).Error().Err(err).Msg("failed to send event to tinybird")
143 }
144 }
145
146 return nil
147 }
148 if err := backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 3)); err != nil {
149 c.JSON(http.StatusOK, gin.H{"message": "url not reachable"})
150
151 return
152 }
153
154 c.JSON(http.StatusOK, res)
155}