Files
kurious/cmd/kuriweb/http.go
Aleksandr Trushkin c7fada2c54 update trace logic
2024-09-24 21:59:33 +03:00

225 lines
6.0 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"
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
"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, "index", "/", coursesAPI.Index).Methods(http.MethodGet)
muxHandleFunc(coursesRouter, "list_learning", coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
muxHandleFunc(coursesRouter, "list_full", 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)
})
}
}
type attributeStringKey string
func (k attributeStringKey) Value(value string) attribute.KeyValue {
return attribute.String(string(k), value)
}
func middlewareTrace() mux.MiddlewareFunc {
reqidAttr := attributeStringKey("http.request_id")
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
route := mux.CurrentRoute(r)
hpath, _ := route.GetPathTemplate()
ctx, span = webtracer.Start(
ctx, r.Method+" "+hpath,
trace.WithAttributes(
reqidAttr.Value(reqid),
semconv.HTTPRequestMethodOriginal(r.Method),
semconv.URLFull(hpath),
semconv.URLPath(r.URL.Path),
semconv.URLQuery(r.URL.RawQuery),
semconv.UserAgentOriginal(r.UserAgent()),
),
trace.WithSpanKind(trace.SpanKindServer),
)
defer span.End()
next.ServeHTTP(w, r.WithContext(ctx))
if wr, ok := w.(*customResponseWriter); ok {
statusCode := xdefault.WithFallback(wr.statusCode, http.StatusOK)
span.SetAttributes(
semconv.HTTPResponseStatusCode(statusCode),
semconv.HTTPResponseBodySize(wr.wroteBytes),
)
if statusCode > 399 {
span.SetStatus(codes.Error, "error during request")
} else {
span.SetStatus(codes.Ok, "request completed")
}
}
})
}
}
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
}