Openstatus www.openstatus.dev

feat(logs): add structured logs (#558)

authored by

Arthur EICHELBERGER and committed by
GitHub
cb04ebde b92a539d

+84 -45
+23 -17
apps/checker/cmd/main.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "errors" 5 6 "fmt" 6 7 "net/http" 7 8 "os" ··· 11 12 12 13 "github.com/gin-gonic/gin" 13 14 "github.com/openstatushq/openstatus/apps/checker" 15 + "github.com/openstatushq/openstatus/apps/checker/pkg/logger" 14 16 "github.com/openstatushq/openstatus/apps/checker/request" 17 + "github.com/rs/zerolog/log" 15 18 ) 16 19 17 20 func main() { ··· 29 32 // environment variables. 30 33 flyRegion := env("FLY_REGION", "local") 31 34 cronSecret := env("CRON_SECRET", "") 35 + logLevel := env("LOG_LEVEL", "warn") 36 + 37 + logger.Configure(logLevel) 32 38 33 39 httpClient := &http.Client{} 34 40 defer httpClient.CloseIdleConnections() ··· 36 42 router := gin.New() 37 43 router.POST("/checker", func(c *gin.Context) { 38 44 ctx := c.Request.Context() 39 - _ = ctx // TODO: use ctx. 40 45 41 46 if c.GetHeader("Authorization") != fmt.Sprintf("Basic %s", cronSecret) { 42 47 c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) ··· 55 60 56 61 var req request.CheckerRequest 57 62 if err := c.ShouldBindJSON(&req); err != nil { 63 + log.Ctx(ctx).Error().Err(err).Msg("failed to decode checker request") 58 64 c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) 59 65 return 60 66 } 61 67 62 - response, error := checker.Ping(httpClient, req) 63 - if error != nil { 68 + response, err := checker.Ping(ctx, httpClient, req) 69 + if err != nil { 64 70 // Add one more retry 65 - response, error = checker.Ping(httpClient, req) 66 - if error != nil { 67 - checker.SendToTinyBird(checker.PingData{ 71 + response, err = checker.Ping(ctx, httpClient, req) 72 + if err != nil { 73 + checker.SendToTinyBird(ctx, checker.PingData{ 68 74 URL: req.URL, 69 75 Region: flyRegion, 70 - Message: error.Error(), 76 + Message: err.Error(), 71 77 CronTimestamp: req.CronTimestamp, 72 78 Timestamp: req.CronTimestamp, 73 79 MonitorID: req.MonitorID, 74 80 WorkspaceID: req.WorkspaceID, 75 81 }) 76 82 if req.Status == "active" { 77 - checker.UpdateStatus(checker.UpdateData{ 83 + checker.UpdateStatus(ctx, checker.UpdateData{ 78 84 MonitorId: req.MonitorID, 79 85 Status: "error", 80 - Message: error.Error(), 86 + Message: err.Error(), 81 87 Region: flyRegion, 82 88 }) 83 89 } ··· 89 95 90 96 if response.StatusCode < 200 || response.StatusCode >= 300 { 91 97 // Add one more retry 92 - response, error = checker.Ping(httpClient, req) 98 + response, err = checker.Ping(ctx, httpClient, req) 93 99 if response.StatusCode < 200 || response.StatusCode >= 300 && req.Status == "active" { 94 - // If the status code is not within the 200 range, we update the status to error 95 - checker.UpdateStatus(checker.UpdateData{ 100 + // If the status code is not within the 200 range, we update the status to err 101 + checker.UpdateStatus(ctx, checker.UpdateData{ 96 102 MonitorId: req.MonitorID, 97 103 Status: "error", 98 104 StatusCode: response.StatusCode, ··· 104 110 // If the status was error and the status code is within the 200 range, we update the status to active 105 111 if req.Status == "error" && response.StatusCode >= 200 && response.StatusCode < 300 { 106 112 // If the status was error, we update it to active 107 - checker.UpdateStatus(checker.UpdateData{ 113 + checker.UpdateStatus(ctx, checker.UpdateData{ 108 114 MonitorId: req.MonitorID, 109 115 Status: "active", 110 116 Region: flyRegion, ··· 112 118 }) 113 119 } 114 120 // We send the data to Tinybird 115 - checker.SendToTinyBird(response) 121 + checker.SendToTinyBird(ctx, response) 116 122 117 123 c.JSON(http.StatusOK, gin.H{"message": "ok"}) 118 124 return ··· 129 135 } 130 136 131 137 go func() { 132 - if err := httpServer.ListenAndServe(); err != nil { 133 - // Add structured logs. 138 + if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 139 + log.Ctx(ctx).Error().Err(err).Msg("failed to start http server") 134 140 cancel() 135 141 } 136 142 }() 137 143 138 144 <-ctx.Done() 139 145 if err := httpServer.Shutdown(ctx); err != nil { 140 - // Add structured logs. 146 + log.Ctx(ctx).Error().Err(err).Msg("failed to shutdown http server") 141 147 return 142 148 } 143 149 }
+3 -2
apps/checker/go.mod
··· 4 4 5 5 require ( 6 6 github.com/gin-gonic/gin v1.9.1 7 - github.com/go-chi/chi/v5 v5.0.10 7 + github.com/rs/zerolog v1.31.0 8 8 ) 9 9 10 10 require ( ··· 19 19 github.com/json-iterator/go v1.1.12 // indirect 20 20 github.com/klauspost/cpuid/v2 v2.2.4 // indirect 21 21 github.com/leodido/go-urn v1.2.4 // indirect 22 + github.com/mattn/go-colorable v0.1.13 // indirect 22 23 github.com/mattn/go-isatty v0.0.19 // indirect 23 24 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 24 25 github.com/modern-go/reflect2 v1.0.2 // indirect ··· 28 29 golang.org/x/arch v0.3.0 // indirect 29 30 golang.org/x/crypto v0.9.0 // indirect 30 31 golang.org/x/net v0.10.0 // indirect 31 - golang.org/x/sys v0.8.0 // indirect 32 + golang.org/x/sys v0.12.0 // indirect 32 33 golang.org/x/text v0.9.0 // indirect 33 34 google.golang.org/protobuf v1.30.0 // indirect 34 35 gopkg.in/yaml.v3 v3.0.1 // indirect
+12 -4
apps/checker/go.sum
··· 4 4 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 5 5 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 6 6 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 7 + github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 7 8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 9 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 10 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= ··· 13 14 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 14 15 github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 15 16 github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 16 - github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= 17 - github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 18 17 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 19 18 github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 20 19 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= ··· 25 24 github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 26 25 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 27 26 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 27 + github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 28 28 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 29 29 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 30 30 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= ··· 36 36 github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 37 37 github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 38 38 github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 39 + github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 40 + github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 41 + github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 39 42 github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 40 43 github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 44 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= ··· 45 48 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 46 49 github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 47 50 github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 51 + github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 48 52 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 53 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 + github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 55 + github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= 56 + github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 50 57 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 58 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 52 59 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= ··· 70 77 golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 71 78 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 72 79 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 + golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 81 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 - golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 75 - golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 82 + golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 83 + golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 84 golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 77 85 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 78 86 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+18 -14
apps/checker/ping.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 + "context" 5 6 "encoding/json" 6 7 "fmt" 7 8 "io" ··· 11 12 "time" 12 13 13 14 "github.com/openstatushq/openstatus/apps/checker/request" 15 + "github.com/rs/zerolog/log" 14 16 ) 15 17 16 18 type PingData struct { ··· 25 27 Message string `json:"message,omitempty"` 26 28 } 27 29 28 - func SendToTinyBird(pingData PingData) { 30 + func SendToTinyBird(ctx context.Context, pingData PingData) { 29 31 url := "https://api.tinybird.co/v0/events?name=ping_response__v5" 30 32 fmt.Printf("📈 Sending data to Tinybird for %+v \n", pingData) 31 33 bearer := "Bearer " + os.Getenv("TINYBIRD_TOKEN") ··· 38 40 client := &http.Client{Timeout: time.Second * 10} 39 41 _, err = client.Do(req) 40 42 if err != nil { 41 - fmt.Println(err) 42 - panic(err) 43 + log.Ctx(ctx).Error().Err(err).Msg("Error while sending data to Tinybird") 43 44 } 44 45 // Should we add a retry mechanism here? 45 46 46 47 } 47 48 48 - func Ping(client *http.Client, inputData request.CheckerRequest) (PingData, error) { 49 + func Ping(ctx context.Context, client *http.Client, inputData request.CheckerRequest) (PingData, error) { 50 + logger := log.Ctx(ctx).With().Str("monitor", inputData.URL).Logger() 49 51 50 52 region := os.Getenv("FLY_REGION") 51 - request, err := http.NewRequest(inputData.Method, inputData.URL, bytes.NewReader([]byte(inputData.Body))) 53 + req, err := http.NewRequestWithContext(ctx, inputData.Method, inputData.URL, bytes.NewReader([]byte(inputData.Body))) 52 54 if err != nil { 53 - return PingData{}, fmt.Errorf("Unable to create request: %w", err) 55 + logger.Error().Err(err).Msg("error while creating req") 56 + return PingData{}, fmt.Errorf("unable to create req: %w", err) 54 57 } 55 58 56 - request.Header.Set("User-Agent", "OpenStatus/1.0") 59 + req.Header.Set("User-Agent", "OpenStatus/1.0") 57 60 58 61 // Setting headers 59 62 for _, header := range inputData.Headers { 60 63 if header.Key != "" && header.Value != "" { 61 - request.Header.Set(header.Key, header.Value) 64 + req.Header.Set(header.Key, header.Value) 62 65 } 63 66 } 64 67 65 68 start := time.Now() 66 - response, err := client.Do(request) 69 + response, err := client.Do(req) 67 70 latency := time.Since(start).Milliseconds() 68 71 69 72 if err != nil { ··· 81 84 } 82 85 } 83 86 84 - return PingData{}, fmt.Errorf("Error with monitor %s: %w", inputData.URL, err) 87 + logger.Error().Err(err).Msg("error while pinging") 88 + return PingData{}, fmt.Errorf("error with monitor %s: %w", inputData.URL, err) 85 89 } 86 90 defer response.Body.Close() 87 91 88 - _, err = io.ReadAll(response.Body) 89 - 90 - if err != nil { 91 - return PingData{}, fmt.Errorf("Error while reading body from %s: %w", inputData.URL, err) 92 + if _, err := io.ReadAll(response.Body); err != nil { 93 + logger.Error().Err(err).Str("monitor", inputData.URL).Msg("error while reading body") 94 + return PingData{}, fmt.Errorf("error while reading body from %s: %w", inputData.URL, err) 92 95 } 96 + 93 97 return PingData{ 94 98 Latency: latency, 95 99 StatusCode: response.StatusCode,
+2 -1
apps/checker/ping_test.go
··· 1 1 package checker 2 2 3 3 import ( 4 + "context" 4 5 "net/http" 5 6 "testing" 6 7 ··· 26 27 } 27 28 for _, tt := range tests { 28 29 t.Run(tt.name, func(t *testing.T) { 29 - got, err := Ping(tt.args.client, tt.args.inputData) 30 + got, err := Ping(context.Background(), tt.args.client, tt.args.inputData) 30 31 31 32 if (err != nil) != tt.wantErr { 32 33 t.Errorf("Ping() error = %v, wantErr %v", err, tt.wantErr)
+19
apps/checker/pkg/logger/logger.go
··· 1 + package logger 2 + 3 + import ( 4 + "github.com/rs/zerolog" 5 + "github.com/rs/zerolog/log" 6 + ) 7 + 8 + func Configure(logLevel string) { 9 + level, err := zerolog.ParseLevel(logLevel) 10 + if err != nil { 11 + level = zerolog.InfoLevel 12 + } 13 + zerolog.SetGlobalLevel(level) 14 + 15 + zerolog.DefaultContextLogger = func() *zerolog.Logger { 16 + logger := log.With().Caller().Logger() 17 + return &logger 18 + }() 19 + }
+7 -7
apps/checker/update.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 + "context" 5 6 "encoding/json" 6 - "fmt" 7 7 "net/http" 8 8 "os" 9 9 "time" 10 + 11 + "github.com/rs/zerolog/log" 10 12 ) 11 13 12 14 type UpdateData struct { ··· 17 19 Region string `json:"region"` 18 20 } 19 21 20 - func UpdateStatus(updateData UpdateData) { 22 + func UpdateStatus(ctx context.Context, updateData UpdateData) { 21 23 url := "https://openstatus-api.fly.dev/updateStatus" 22 - fmt.Println("URL:>", url) 23 24 basic := "Basic " + os.Getenv("CRON_SECRET") 24 25 payloadBuf := new(bytes.Buffer) 25 26 json.NewEncoder(payloadBuf).Encode(updateData) 26 - req, err := http.NewRequest("POST", url, payloadBuf) 27 + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, payloadBuf) 27 28 req.Header.Set("Authorization", basic) 28 29 req.Header.Set("Content-Type", "application/json") 29 30 30 31 client := &http.Client{Timeout: time.Second * 10} 31 - _, err = client.Do(req) 32 - if err != nil { 33 - panic(err) 32 + if _, err = client.Do(req); err != nil { 33 + log.Ctx(ctx).Error().Err(err).Msg("error while updating status") 34 34 } 35 35 // Should we add a retry mechanism here? 36 36 }