Brijesh's Git Server — watchman @ cd48349fb70225313368346a4b01af65c5a3b7b0

observability tool, needs to be rewritten once identity is stable

infra(batman): create basic structure and middleware

created a basic file structure, its not ideal but good enough for now

also copied code for rate limiting middleware, might have to change it if I decide to use a reverse proxy

and a simple request ID middleware
Brijesh ops@brijesh.dev
Tue, 25 Jun 2024 14:48:53 +0530
commit

cd48349fb70225313368346a4b01af65c5a3b7b0

A .env

@@ -0,0 +1,1 @@

+PORT=4000
A go.mod

@@ -0,0 +1,9 @@

+module watchman + +go 1.22.4 + +require ( + github.com/joho/godotenv v1.5.0 + github.com/google/uuid v1.6.0 + golang.org/x/time v0.5.0 +)
A go.sum

@@ -0,0 +1,6 @@

+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.0 h1:C/Vohk/9L1RCoS/UW2gfyi2N0EElSW3yb9zwi3PjosE= +github.com/joho/godotenv v1.5.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
A main.go

@@ -0,0 +1,40 @@

+package main + +import ( + "fmt" + "log" + "net/http" + "watchman/middleware" + "watchman/utils" +) + +const ( + RED = "\033[31m" + RESET_COLOUR = "\033[0m" +) + +func init() { + env_vars := []string{"PORT"} + + for _, env_var := range env_vars { + if !utils.Verify_ENV_Exists(env_var) { + log.Fatal(RED + "Error: " + env_var + " not found in .env file" + RESET_COLOUR) + } + } +} + +func main() { + port := utils.Load_ENV("PORT") + + multiplexer := http.NewServeMux() + + multiplexer.HandleFunc("/", utils.Example_Handler) + multiplexer.HandleFunc("/health", utils.Health_Check_Handler) + + fmt.Println("Starting server on " + port) + log.Fatal(http.ListenAndServe(":"+port, + middleware.RequestIDMiddleware( + middleware.Ratelimit( + multiplexer, + )))) +}
A middleware/rate_limiting.go

@@ -0,0 +1,94 @@

+package middleware + +import ( + "encoding/json" + "log" + "net" + "net/http" + "sync" + "time" + "watchman/schema" + + "golang.org/x/time/rate" +) + +// Defining visitor struct for use in array of unique visitors +type visitor struct { + limiter *rate.Limiter + lastSeen time.Time +} + +// The visitors map is used to keep track of the visitors based on their IP addresses +// The mu mutex is used to protect the visitors map from concurrent reads and writes +var ( + visitors = make(map[string]*visitor) + mu sync.Mutex +) + +// Run cleanup function in a background go routine +func init() { + go cleanupVisitors() +} + +// Get the visitor from the visitors map based on the IP address +func getVisitor(ip string) *rate.Limiter { + mu.Lock() + defer mu.Unlock() + + v, exists := visitors[ip] + if !exists { + limiter := rate.NewLimiter(1, 3) + // Include the current time when creating a new visitor + visitors[ip] = &visitor{limiter, time.Now()} + return limiter + } + + // Update the last seen time for the visitor + v.lastSeen = time.Now() + return v.limiter +} + +// Delete the visitor if it was last seen over 3 minutes ago +func cleanupVisitors() { + for { + time.Sleep(time.Minute) + + mu.Lock() + for ip, v := range visitors { + if time.Since(v.lastSeen) > 3*time.Minute { + delete(visitors, ip) + } + } + mu.Unlock() + } +} + +func Ratelimit(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + log.Print(err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + limiter := getVisitor(ip) + if !limiter.Allow() { + response := schema.Response_Type{ + Status: "ERROR", + Message: "You made too many requests", + RequestID: r.Context().Value(schema.RequestIDKey{}).(string), + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + return + } + + next.ServeHTTP(w, r) + }) +}
A middleware/request_id.go

@@ -0,0 +1,25 @@

+package middleware + +import ( + "context" + "net/http" + "watchman/schema" + + "github.com/google/uuid" +) + +// NEEDS WORK: wrote a struct for RequestIDKey instead of "RequestID" string due to LSP warning to +// not use string key to avoid potential collisions, but a string is used in the middleware function +// for request IDs in echo's middleware package, not sure what is the best way to handle this +// RequestIDKey is moved to schema package + +func RequestIDMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestID := uuid.New().String() + ctx := context.WithValue(r.Context(), schema.RequestIDKey{}, requestID) + + // ctx := context.WithValue(r.Context(), "requestID", requestID) + w.Header().Set("X-Request-ID", requestID) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +}
A schema/types.go

@@ -0,0 +1,9 @@

+package schema + +type Response_Type struct { + Status string `json:"status"` + Message string `json:"message"` + RequestID string `json:"request_id"` +} + +type RequestIDKey struct{}
A utils/common_handlers.go

@@ -0,0 +1,25 @@

+package utils + +import ( + "encoding/json" + "net/http" + "watchman/schema" +) + +func Health_Check_Handler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK")) +} + +func Example_Handler(w http.ResponseWriter, r *http.Request) { + response := schema.Response_Type{ + Status: "OK", + Message: "Everything is fine", + RequestID: r.Context().Value(schema.RequestIDKey{}).(string), + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +}
A utils/env_vars.go

@@ -0,0 +1,28 @@

+package utils + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func Load_ENV(env_var string) string { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + return os.Getenv(env_var) +} + +func Verify_ENV_Exists(env_var string) bool { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + if os.Getenv(env_var) == "" { + return false + } + return true +}