use errx and splinter packages
This commit is contained in:
parent
cb373e637b
commit
75a4590f18
7 changed files with 95 additions and 26 deletions
5
go.mod
5
go.mod
|
|
@ -1,3 +1,8 @@
|
||||||
module git.juancwu.dev/juancwu/lightmux
|
module git.juancwu.dev/juancwu/lightmux
|
||||||
|
|
||||||
go 1.26.2
|
go 1.26.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.juancwu.dev/juancwu/errx v0.1.0
|
||||||
|
git.juancwu.dev/juancwu/splinter v0.1.0
|
||||||
|
)
|
||||||
|
|
|
||||||
4
go.sum
Normal file
4
go.sum
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
git.juancwu.dev/juancwu/errx v0.1.0 h1:92yA0O1BkKGXcoEiWtxwH/ztXCjoV1KSTMtKpm3gd2w=
|
||||||
|
git.juancwu.dev/juancwu/errx v0.1.0/go.mod h1:7jNhBOwcZ/q7zDD6mln3QCJBYZ8T6h+dAdxVfykprTk=
|
||||||
|
git.juancwu.dev/juancwu/splinter v0.1.0 h1:ZGvvzyi24hZw/yFAwpUsHtj+q+fh9I2KIGmOAILWD5Q=
|
||||||
|
git.juancwu.dev/juancwu/splinter v0.1.0/go.mod h1:dAYsRQfS6tqWynEGz8xMCtIJUN7+KIp3jLE7kgO3yKE=
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.juancwu.dev/juancwu/splinter"
|
||||||
)
|
)
|
||||||
|
|
||||||
type statusRecorder struct {
|
type statusRecorder struct {
|
||||||
|
|
@ -33,6 +34,11 @@ func Logger(next http.Handler) http.Handler {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
|
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
|
||||||
next.ServeHTTP(rec, r)
|
next.ServeHTTP(rec, r)
|
||||||
log.Printf("%s %s %d %s", r.Method, r.URL.Path, rec.status, time.Since(start))
|
splinter.Info("http.request",
|
||||||
|
"method", r.Method,
|
||||||
|
"path", r.URL.Path,
|
||||||
|
"status", rec.status,
|
||||||
|
"duration", time.Since(start),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,29 @@ package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.juancwu.dev/juancwu/splinter"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLogger(t *testing.T) {
|
func captureSplinter(t *testing.T) *bytes.Buffer {
|
||||||
|
t.Helper()
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
orig := log.Default().Writer()
|
logger := splinter.New(splinter.WithStream(splinter.NewConsoleStream(
|
||||||
log.Default().SetOutput(&buf)
|
splinter.ConsoleJSON,
|
||||||
defer log.Default().SetOutput(orig)
|
splinter.LevelDebug,
|
||||||
|
splinter.ConsoleWriter(&buf),
|
||||||
|
)))
|
||||||
|
prev := splinter.SetDefault(logger)
|
||||||
|
t.Cleanup(func() { splinter.SetDefault(prev) })
|
||||||
|
return &buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger(t *testing.T) {
|
||||||
|
buf := captureSplinter(t)
|
||||||
|
|
||||||
h := Logger(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
h := Logger(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusTeapot)
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
|
@ -26,23 +37,22 @@ func TestLogger(t *testing.T) {
|
||||||
t.Errorf("status code = %d, want 418", rr.Code)
|
t.Errorf("status code = %d, want 418", rr.Code)
|
||||||
}
|
}
|
||||||
out := buf.String()
|
out := buf.String()
|
||||||
if !strings.Contains(out, "GET /foo 418") {
|
for _, want := range []string{`"method":"GET"`, `"path":"/foo"`, `"status":418`} {
|
||||||
t.Errorf("log output missing expected fields: %q", out)
|
if !strings.Contains(out, want) {
|
||||||
|
t.Errorf("log output missing %s\nfull output: %s", want, out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerDefaultStatusOK(t *testing.T) {
|
func TestLoggerDefaultStatusOK(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
buf := captureSplinter(t)
|
||||||
orig := log.Default().Writer()
|
|
||||||
log.Default().SetOutput(&buf)
|
|
||||||
defer log.Default().SetOutput(orig)
|
|
||||||
|
|
||||||
h := Logger(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
h := Logger(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte("hi"))
|
w.Write([]byte("hi"))
|
||||||
}))
|
}))
|
||||||
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
|
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
|
||||||
|
|
||||||
if !strings.Contains(buf.String(), "200") {
|
if !strings.Contains(buf.String(), `"status":200`) {
|
||||||
t.Errorf("expected default 200 in log, got %q", buf.String())
|
t.Errorf("expected default status 200 in log, got %q", buf.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,27 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
|
"git.juancwu.dev/juancwu/errx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const recovererOp = "middleware.Recoverer"
|
||||||
|
|
||||||
func Recoverer(next http.Handler) http.Handler {
|
func Recoverer(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if rec := recover(); rec != nil {
|
rec := recover()
|
||||||
log.Printf("panic: %v\n%s", rec, debug.Stack())
|
if rec == nil {
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
return
|
||||||
}
|
}
|
||||||
|
var err error
|
||||||
|
if e, ok := rec.(error); ok {
|
||||||
|
err = errx.Wrap(recovererOp, e)
|
||||||
|
} else {
|
||||||
|
err = errx.Newf(recovererOp, "panic: %v", rec)
|
||||||
|
}
|
||||||
|
log.Printf("%v\n%s", err, debug.Stack())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
}()
|
}()
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
@ -9,11 +10,17 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRecovererCatchesPanic(t *testing.T) {
|
func captureLog(t *testing.T) *bytes.Buffer {
|
||||||
|
t.Helper()
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
orig := log.Default().Writer()
|
orig := log.Default().Writer()
|
||||||
log.Default().SetOutput(&buf)
|
log.Default().SetOutput(&buf)
|
||||||
defer log.Default().SetOutput(orig)
|
t.Cleanup(func() { log.Default().SetOutput(orig) })
|
||||||
|
return &buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecovererCatchesStringPanic(t *testing.T) {
|
||||||
|
buf := captureLog(t)
|
||||||
|
|
||||||
h := Recoverer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
h := Recoverer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
panic("boom")
|
panic("boom")
|
||||||
|
|
@ -25,8 +32,27 @@ func TestRecovererCatchesPanic(t *testing.T) {
|
||||||
if rr.Code != http.StatusInternalServerError {
|
if rr.Code != http.StatusInternalServerError {
|
||||||
t.Errorf("status = %d, want 500", rr.Code)
|
t.Errorf("status = %d, want 500", rr.Code)
|
||||||
}
|
}
|
||||||
if !strings.Contains(buf.String(), "panic: boom") {
|
out := buf.String()
|
||||||
t.Errorf("expected panic log, got %q", buf.String())
|
for _, want := range []string{"middleware.Recoverer", "panic: boom"} {
|
||||||
|
if !strings.Contains(out, want) {
|
||||||
|
t.Errorf("log missing %q\nfull: %s", want, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecovererWrapsErrorPanic(t *testing.T) {
|
||||||
|
buf := captureLog(t)
|
||||||
|
|
||||||
|
cause := errors.New("db down")
|
||||||
|
h := Recoverer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
panic(cause)
|
||||||
|
}))
|
||||||
|
|
||||||
|
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
|
||||||
|
|
||||||
|
out := buf.String()
|
||||||
|
if !strings.Contains(out, "middleware.Recoverer: db down") {
|
||||||
|
t.Errorf("expected errx-wrapped breadcrumb, got: %s", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
package router
|
package router
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.juancwu.dev/juancwu/errx"
|
||||||
|
)
|
||||||
|
|
||||||
|
const groupOp = "router.Group"
|
||||||
|
|
||||||
func splitPattern(pattern string) (method, host, path string) {
|
func splitPattern(pattern string) (method, host, path string) {
|
||||||
rest := pattern
|
rest := pattern
|
||||||
|
|
@ -22,10 +28,10 @@ func validateGroupPrefix(p string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.ContainsAny(p, " \t") {
|
if strings.ContainsAny(p, " \t") {
|
||||||
panic("lightmux: group prefix must not contain whitespace (no method or host allowed): " + p)
|
panic(errx.Newf(groupOp, "prefix must not contain whitespace (no method or host allowed): %q", p))
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(p, "/") {
|
if !strings.HasPrefix(p, "/") {
|
||||||
panic("lightmux: group prefix must start with '/': " + p)
|
panic(errx.Newf(groupOp, "prefix must start with '/': %q", p))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +59,7 @@ func joinPath(prefix, sub string) string {
|
||||||
return prefix + "/"
|
return prefix + "/"
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(sub, "/") {
|
if !strings.HasPrefix(sub, "/") {
|
||||||
panic("lightmux: route path must start with '/': " + sub)
|
panic(errx.Newf(groupOp, "route path must start with '/': %q", sub))
|
||||||
}
|
}
|
||||||
return prefix + sub
|
return prefix + sub
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue