Files
remark42/backend/vendor/github.com/go-pkgz/rest/logger/logger.go
T
dependabot[bot]GitHubpaskaldependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
638fa63e81 Bump the go-modules-updates group in /backend with 7 updates (#1995)
* Bump the go-modules-updates group in /backend with 7 updates

Bumps the go-modules-updates group in /backend with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) | `2.21.1` | `2.23.1` |
| [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.2.3` | `5.2.4` |
| [github.com/go-pkgz/rest](https://github.com/go-pkgz/rest) | `1.20.6` | `1.21.0` |
| [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) | `5.3.0` | `5.3.1` |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.46.0` | `0.47.0` |
| [golang.org/x/image](https://github.com/golang/image) | `0.34.0` | `0.35.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.48.0` | `0.49.0` |


Updates `github.com/alecthomas/chroma/v2` from 2.21.1 to 2.23.1
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.21.1...v2.23.1)

Updates `github.com/go-chi/chi/v5` from 5.2.3 to 5.2.4
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.2.3...v5.2.4)

Updates `github.com/go-pkgz/rest` from 1.20.6 to 1.21.0
- [Release notes](https://github.com/go-pkgz/rest/releases)
- [Commits](https://github.com/go-pkgz/rest/compare/v1.20.6...v1.21.0)

Updates `github.com/golang-jwt/jwt/v5` from 5.3.0 to 5.3.1
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.3.0...v5.3.1)

Updates `golang.org/x/crypto` from 0.46.0 to 0.47.0
- [Commits](https://github.com/golang/crypto/compare/v0.46.0...v0.47.0)

Updates `golang.org/x/image` from 0.34.0 to 0.35.0
- [Commits](https://github.com/golang/image/compare/v0.34.0...v0.35.0)

Updates `golang.org/x/net` from 0.48.0 to 0.49.0
- [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-version: 2.23.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/go-chi/chi/v5
  dependency-version: 5.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: github.com/go-pkgz/rest
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-version: 5.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/crypto
  dependency-version: 0.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/image
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
...

Signed-off-by: dependabot[bot] <support@github.com>

* Run go mod tidy in examples directory

Co-authored-by: paskal <712534+paskal@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: paskal <712534+paskal@users.noreply.github.com>
2026-02-14 19:48:24 -06:00

360 lines
8.0 KiB
Go

// Package logger implements logging middleware
package logger
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"github.com/go-pkgz/rest/realip"
)
// Middleware is a logger for rest requests.
type Middleware struct {
prefix string
logBody bool
maxBodySize int
ipFn func(ip string) string
userFn func(r *http.Request) (string, error)
subjFn func(r *http.Request) (string, error)
log Backend
apacheCombined bool
}
// Backend is logging backend
type Backend interface {
Logf(format string, args ...any)
}
type logParts struct {
duration time.Duration
rawURL string
method string
remoteIP string
statusCode int
respSize int
host string
prefix string
user string
body string
}
type stdBackend struct{}
func (s stdBackend) Logf(format string, args ...any) {
log.Printf(format, args...)
}
// Logger is a default logger middleware with "REST" prefix
func Logger(next http.Handler) http.Handler {
l := New(Prefix("REST"))
return l.Handler(next)
}
// New makes rest logger with given options
func New(options ...Option) *Middleware {
res := Middleware{
prefix: "",
maxBodySize: 1024,
log: stdBackend{},
}
for _, opt := range options {
opt(&res)
}
return &res
}
// Handler middleware prints http log
func (l *Middleware) Handler(next http.Handler) http.Handler {
formater := l.formatDefault
if l.apacheCombined {
formater = l.formatApacheCombined
}
fn := func(w http.ResponseWriter, r *http.Request) {
ww := newCustomResponseWriter(w)
user := ""
if l.userFn != nil {
if u, err := l.userFn(r); err == nil {
user = u
}
}
body := l.getBody(r)
t1 := time.Now()
defer func() {
t2 := time.Now()
u := *r.URL // shallow copy
u.RawQuery = l.sanitizeQuery(u.RawQuery)
rawurl := u.String()
if unescURL, err := url.QueryUnescape(rawurl); err == nil {
rawurl = unescURL
}
remoteIP, err := realip.Get(r)
if err != nil {
remoteIP = "unknown ip"
}
if l.ipFn != nil { // mask ip with ipFn
remoteIP = l.ipFn(remoteIP)
}
server := r.URL.Hostname()
if server == "" {
server = strings.Split(r.Host, ":")[0]
}
p := &logParts{
duration: t2.Sub(t1),
rawURL: rawurl,
method: r.Method,
host: server,
remoteIP: remoteIP,
statusCode: ww.status,
respSize: ww.size,
prefix: l.prefix,
user: user,
body: body,
}
l.log.Logf(formater(r, p))
}()
next.ServeHTTP(ww, r)
}
return http.HandlerFunc(fn)
}
func (l *Middleware) formatDefault(r *http.Request, p *logParts) string {
var bld strings.Builder
if l.prefix != "" {
_, _ = bld.WriteString(l.prefix)
_, _ = bld.WriteString(" ")
}
_, _ = bld.WriteString(fmt.Sprintf("%s - %s - %s - %s - %d (%d) - %v",
p.method, p.rawURL, p.host, p.remoteIP, p.statusCode, p.respSize, p.duration))
if p.user != "" {
_, _ = bld.WriteString(" - ")
_, _ = bld.WriteString(p.user)
}
if l.subjFn != nil {
if subj, err := l.subjFn(r); err == nil {
_, _ = bld.WriteString(" - ")
_, _ = bld.WriteString(subj)
}
}
if traceID := r.Header.Get("X-Request-ID"); traceID != "" {
_, _ = bld.WriteString(" - ")
_, _ = bld.WriteString(traceID)
}
if p.body != "" {
_, _ = bld.WriteString(" - ")
_, _ = bld.WriteString(p.body)
}
return bld.String()
}
// 127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
// nolint gosec
func (l *Middleware) formatApacheCombined(r *http.Request, p *logParts) string {
username := "-"
if p.user != "" {
username = p.user
}
var bld strings.Builder
bld.WriteString(p.remoteIP)
bld.WriteString(" - ")
bld.WriteString(username)
bld.WriteString(" [")
bld.WriteString(time.Now().Format("02/Jan/2006:15:04:05 -0700"))
bld.WriteString(`] "`)
bld.WriteString(p.method)
bld.WriteString(" ")
bld.WriteString(p.rawURL)
bld.WriteString(`" `)
bld.WriteString(r.Proto)
bld.WriteString(`" `)
bld.WriteString(strconv.Itoa(p.statusCode))
bld.WriteString(" ")
bld.WriteString(strconv.Itoa(p.respSize))
bld.WriteString(` "`)
bld.WriteString(r.Header.Get("Referer"))
bld.WriteString(`" "`)
bld.WriteString(r.Header.Get("User-Agent"))
bld.WriteString(`"`)
return bld.String()
}
var reMultWhtsp = regexp.MustCompile(`[\s\p{Zs}]{2,}`)
func (l *Middleware) getBody(r *http.Request) string {
if !l.logBody {
return ""
}
reader, body, hasMore, err := peek(r.Body, int64(l.maxBodySize))
if err != nil {
return ""
}
// "The Server will close the request body. The ServeHTTP Handler does not need to."
// https://golang.org/pkg/net/http/#Request
// so we can use ioutil.NopCloser() to make io.ReadCloser.
// note that below assignment is not approved by the docs:
// "Except for reading the body, handlers should not modify the provided Request."
// https://golang.org/pkg/net/http/#Handler
r.Body = io.NopCloser(reader)
if body != "" {
body = strings.ReplaceAll(body, "\n", " ")
body = reMultWhtsp.ReplaceAllString(body, " ")
}
if hasMore {
body += "..."
}
return body
}
// peek the first n bytes as string
func peek(r io.Reader, n int64) (reader io.Reader, s string, hasMore bool, err error) {
if n < 0 {
n = 0
}
buf := new(bytes.Buffer)
_, err = io.CopyN(buf, r, n+1)
if err == io.EOF {
str := buf.String()
return buf, str, false, nil
}
if err != nil {
return r, "", false, err
}
// one extra byte is successfully read
s = buf.String()
s = s[:len(s)-1]
return io.MultiReader(buf, r), s, true, nil
}
var keysToHide = []string{"password", "passwd", "secret", "credentials", "token"}
// Hide query values for keysToHide. May change order of query params.
// May escape unescaped query params.
func (l *Middleware) sanitizeQuery(rawQuery string) string {
// note that we skip non-nil error further
query, err := url.ParseQuery(rawQuery)
isHidden := func(key string) bool {
for _, k := range keysToHide {
if strings.EqualFold(k, key) {
return true
}
}
return false
}
present := false
for key, values := range query {
if isHidden(key) {
present = true
for i := range values {
values[i] = "********"
}
}
}
// short circuit
if (err == nil) && !present {
return rawQuery
}
return query.Encode()
}
// AnonymizeIP is a function to reset the last part of IPv4 to 0.
// from 123.212.12.78 it will make 123.212.12.0
func AnonymizeIP(ip string) string {
if ip == "" {
return ""
}
parts := strings.Split(ip, ".")
if len(parts) != 4 {
return ip
}
return strings.Join(parts[:3], ".") + ".0"
}
// customResponseWriter is an HTTP response logger that keeps HTTP status code and
// the number of bytes written.
// It implements http.ResponseWriter, http.Flusher and http.Hijacker.
// Note that type assertion from http.ResponseWriter(customResponseWriter) to
// http.Flusher and http.Hijacker is always succeed but underlying http.ResponseWriter
// may not implement them.
type customResponseWriter struct {
http.ResponseWriter
status int
size int
}
func newCustomResponseWriter(w http.ResponseWriter) *customResponseWriter {
return &customResponseWriter{
ResponseWriter: w,
status: 200,
}
}
// WriteHeader implements http.ResponseWriter and saves status
func (c *customResponseWriter) WriteHeader(status int) {
c.status = status
c.ResponseWriter.WriteHeader(status)
}
// Write implements http.ResponseWriter and tracks number of bytes written
func (c *customResponseWriter) Write(b []byte) (int, error) {
size, err := c.ResponseWriter.Write(b)
c.size += size
return size, err
}
// Flush implements http.Flusher
func (c *customResponseWriter) Flush() {
if f, ok := c.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// Hijack implements http.Hijacker
func (c *customResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := c.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, fmt.Errorf("ResponseWriter does not implement the Hijacker interface")
}