package http import ( "encoding/json" "fmt" "log/slog" "net/http" "net/http/pprof" "time" "git.loyso.art/frx/devsim/internal/entities" "git.loyso.art/frx/devsim/internal/store" ) type handlersBuilder struct { mux *http.ServeMux } func NewHandlersBuilder() *handlersBuilder { return &handlersBuilder{ mux: http.NewServeMux(), } } // MountStatsHandlers mounts stats related handlers. func (h *handlersBuilder) MountStatsHandlers(sr store.Stats, log *slog.Logger) { log = log.With(slog.String("api", "http")) mws := multipleMiddlewares( middlewarePanicRecovery(log), middlewareLogger(log), ) h.mux.Handle("/api/v1/stats/", mws(listStatsHandler(sr))) h.mux.Handle("/api/v1/stats/{id}", mws(postStatsHandler(sr))) } func (s *handlersBuilder) MountProfileHandlers() { s.mux.HandleFunc("/debug/pprof", pprof.Index) s.mux.HandleFunc("/debug/pprof/profile", pprof.Profile) s.mux.HandleFunc("/debug/pprof/trace", pprof.Trace) } func (s *handlersBuilder) Build() http.Handler { return s.mux } // ListenAndServe runs server to accept incoming connections. This function blocks on // handling connections. func listStatsHandler(sr store.Stats) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } stats, err := sr.List(r.Context()) if err != nil { http.Error(w, fmt.Errorf("listing stats: %w", err).Error(), http.StatusInternalServerError) return } w.Header().Set("content-type", "application/json") err = json.NewEncoder(w).Encode(stats) if err != nil { http.Error(w, fmt.Errorf("encoding payload: %w", err).Error(), http.StatusInternalServerError) } }) } func postStatsHandler(sr store.Stats) http.HandlerFunc { type request struct { IncomingTraffic int `json:"incoming_traffic"` OutgoingTraffic int `json:"outgoing_traffic"` IncomingRPS int `json:"incoming_rps"` ReadRPS int `json:"read_rps"` WriteRPS int `json:"write_rps"` } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } id := r.PathValue("id") if id == "" { http.Error(w, "no id provided", http.StatusBadRequest) return } var reqbody request err := json.NewDecoder(r.Body).Decode(&reqbody) if err != nil { http.Error(w, fmt.Errorf("decoding body: %w", err).Error(), http.StatusBadRequest) return } ctx := r.Context() err = sr.Upsert(ctx, entities.DeviceStatistics{ ID: entities.DeviceID(id), IncomingTrafficBytes: reqbody.IncomingTraffic, OutgoingTrafficBytes: reqbody.OutgoingTraffic, IncomingRPS: reqbody.IncomingRPS, ReadRPS: reqbody.ReadRPS, WriteRPS: reqbody.WriteRPS, }) if err != nil { http.Error(w, fmt.Errorf("upserting stat metric: %w", err).Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) }) } type middlewareFunc func(http.Handler) http.Handler func middlewarePanicRecovery(log *slog.Logger) middlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { rec := recover() if rec == nil { return } log.ErrorContext( r.Context(), "panic acquired during request", slog.Any("panic", rec), ) }() next.ServeHTTP(w, r) }) } } func middlewareLogger(log *slog.Logger) middlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() requestID := r.Header.Get("x-request-id") if requestID == "" { requestID = randomID() } w.Header().Set("x-request-id", requestID) method := r.Method path := r.URL.Path query := r.URL.Query().Encode() log.InfoContext( r.Context(), "request processing", slog.String("request_id", requestID), slog.String("method", method), slog.String("path", path), slog.String("query", query), ) next.ServeHTTP(w, r) elapsed := time.Since(start) log.InfoContext( r.Context(), "request finished", slog.Duration("elapsed", elapsed.Truncate(time.Millisecond)), ) }) } } func multipleMiddlewares(mws ...middlewareFunc) middlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for _, mw := range mws { next = mw(next) } next.ServeHTTP(w, r) }) } } func randomID() string { return "" }