205 lines
5.4 KiB
Go
205 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.loyso.art/frx/kurious/assets/kurious"
|
|
"git.loyso.art/frx/kurious/internal/common/config"
|
|
"git.loyso.art/frx/kurious/internal/common/generator"
|
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
|
xhttp "git.loyso.art/frx/kurious/internal/kurious/ports/http"
|
|
"git.loyso.art/frx/kurious/pkg/xdefault"
|
|
|
|
"github.com/gorilla/mux"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
func makePathTemplate(params ...string) string {
|
|
var sb strings.Builder
|
|
for _, param := range params {
|
|
sb.Grow(len(param) + 3)
|
|
|
|
sb.WriteRune('/')
|
|
sb.WriteRune('{')
|
|
sb.WriteString(param)
|
|
sb.WriteRune('}')
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func setupCoursesHTTP(srv xhttp.Server, router *mux.Router, _ *slog.Logger) {
|
|
coursesAPI := srv.Courses()
|
|
|
|
router.Handle("/", http.RedirectHandler("/courses", http.StatusPermanentRedirect))
|
|
|
|
coursesRouter := router.PathPrefix("/courses").Subrouter().StrictSlash(true)
|
|
|
|
coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam)
|
|
coursesListFullPath := makePathTemplate(xhttp.LearningTypePathParam, xhttp.ThematicTypePathParam)
|
|
|
|
muxHandleFunc(coursesRouter, "/", coursesAPI.Index).Methods(http.MethodGet)
|
|
muxHandleFunc(coursesRouter, coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
|
|
muxHandleFunc(coursesRouter, coursesListFullPath, coursesAPI.List).Methods(http.MethodGet)
|
|
}
|
|
|
|
func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server {
|
|
router := mux.NewRouter()
|
|
|
|
router.Use(
|
|
middlewareCustomWriterInjector(),
|
|
mux.CORSMethodMiddleware(router),
|
|
middlewareLogger(log),
|
|
middlewareTrace(),
|
|
)
|
|
|
|
setupCoursesHTTP(srv, router, log)
|
|
|
|
if cfg.MountLive {
|
|
fs := http.FileServer(http.Dir("./assets/kurious/static/"))
|
|
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs)).Methods(http.MethodGet)
|
|
|
|
registerFile := func(filepath string) {
|
|
if !strings.HasPrefix(filepath, "/") {
|
|
filepath = "/" + filepath
|
|
}
|
|
|
|
relativePath := "./assets/kurious" + filepath
|
|
router.HandleFunc(filepath, func(w http.ResponseWriter, r *http.Request) {
|
|
http.ServeFile(w, r, relativePath)
|
|
}).Methods(http.MethodGet)
|
|
}
|
|
|
|
for _, file := range []string{
|
|
"robots.txt",
|
|
"android-chrome-192x192.png",
|
|
"android-chrome-512x512.png",
|
|
"apple-touch-icon.png",
|
|
"favicon-16x16.png",
|
|
"favicon-32x32.png",
|
|
"favicon.ico",
|
|
"site.webmanifest",
|
|
} {
|
|
registerFile(file)
|
|
}
|
|
} else {
|
|
fs := kurious.AsHTTPFileHandler()
|
|
router.PathPrefix("/*").Handler(fs).Methods(http.MethodGet)
|
|
}
|
|
|
|
return &http.Server{
|
|
Addr: cfg.ListenAddr,
|
|
Handler: router,
|
|
}
|
|
}
|
|
|
|
func middlewareCustomWriterInjector() mux.MiddlewareFunc {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
wr := wrapWithCustomWriter(w)
|
|
next.ServeHTTP(wr, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
func middlewareTrace() mux.MiddlewareFunc {
|
|
reqidAttr := attribute.Key("http.request_id")
|
|
statusAttr := attribute.Key("http.status_code")
|
|
payloadAttr := attribute.Key("http.payload_size")
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
reqid := xcontext.GetRequestID(ctx)
|
|
|
|
var span trace.Span
|
|
ctx, span = webtracer.Start(ctx, r.URL.String(), trace.WithAttributes(reqidAttr.String(reqid)))
|
|
defer span.End()
|
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
|
|
if wr, ok := w.(*customResponseWriter); ok {
|
|
statusCode := xdefault.WithFallback(wr.statusCode, http.StatusOK)
|
|
span.SetAttributes(
|
|
statusAttr.Int(statusCode),
|
|
payloadAttr.Int(wr.wroteBytes),
|
|
)
|
|
if statusCode > 399 {
|
|
span.SetStatus(codes.Error, "error during request")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func middlewareLogger(log *slog.Logger) mux.MiddlewareFunc {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
requestID := r.Header.Get("x-request-id")
|
|
if requestID == "" {
|
|
requestID = generator.RandomInt64ID()
|
|
}
|
|
|
|
ctx = xcontext.WithLogFields(
|
|
ctx,
|
|
slog.String("request_id", requestID),
|
|
)
|
|
ctx = xcontext.WithRequestID(ctx, requestID)
|
|
|
|
xcontext.LogInfo(
|
|
ctx, log, "incoming request",
|
|
slog.String("method", r.Method),
|
|
slog.String("path", r.URL.Path),
|
|
)
|
|
|
|
start := time.Now()
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
elapsed := slog.Duration("elapsed", time.Since(start).Truncate(time.Millisecond))
|
|
|
|
logfields := make([]slog.Attr, 0, 3)
|
|
logfields = append(logfields, elapsed)
|
|
if wr, ok := w.(*customResponseWriter); ok {
|
|
statusCode := xdefault.WithFallback(wr.statusCode, http.StatusOK)
|
|
logfields = append(
|
|
logfields,
|
|
slog.Int("status_code", statusCode),
|
|
slog.Int("bytes_wrote", wr.wroteBytes),
|
|
)
|
|
}
|
|
|
|
xcontext.LogInfo(ctx, log, "request processed", logfields...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func wrapWithCustomWriter(origin http.ResponseWriter) *customResponseWriter {
|
|
return &customResponseWriter{
|
|
ResponseWriter: origin,
|
|
}
|
|
}
|
|
|
|
type customResponseWriter struct {
|
|
http.ResponseWriter
|
|
|
|
statusCode int
|
|
wroteBytes int
|
|
}
|
|
|
|
func (w *customResponseWriter) WriteHeader(statusCode int) {
|
|
w.statusCode = statusCode
|
|
w.ResponseWriter.WriteHeader(statusCode)
|
|
}
|
|
|
|
func (w *customResponseWriter) Write(data []byte) (n int, err error) {
|
|
n, err = w.ResponseWriter.Write(data)
|
|
w.wroteBytes += n
|
|
|
|
return n, err
|
|
}
|