mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
Operators could not tell which client sends requests that are refused with 401. Add --log-client (LOCALAI_LOG_CLIENT, off by default). When set, the HTTP request log line also carries duration, remote, user_agent and, when present, request_id. The address is the TCP peer. It is never read from X-Forwarded-For, X-Real-Ip or Forwarded, because LocalAI has no trusted-proxy setting and any client can forge those headers. Behind a reverse proxy the value is the proxy address. Authorization headers, cookies, query strings and bodies are never logged. User agent and request id are cut to a fixed length and have control characters replaced. Assisted-by: Claude:claude-sonnet-5-5 go gofmt
118 lines
3.4 KiB
Go
118 lines
3.4 KiB
Go
package http
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/mudler/LocalAI/pkg/diagnostics"
|
|
"github.com/mudler/xlog"
|
|
)
|
|
|
|
// maxLoggedUserAgent caps the user agent in the access log so a client
|
|
// cannot flood the log with one header.
|
|
const maxLoggedUserAgent = 80
|
|
|
|
// maxLoggedRequestID caps the request id for the same reason.
|
|
const maxLoggedRequestID = 64
|
|
|
|
// accessLog logs one line per request. When logClient is true the line also
|
|
// carries the peer address and the user agent, so an unauthorized caller can
|
|
// be identified.
|
|
func accessLog(logClient bool) echo.MiddlewareFunc {
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
start := time.Now()
|
|
req := c.Request()
|
|
res := c.Response()
|
|
err := next(c)
|
|
|
|
// Echo's central HTTPErrorHandler runs *after* this middleware
|
|
// returns, so res.Status still reads the default 200 here when a
|
|
// handler returned an error without writing a response. Mirror
|
|
// echo.DefaultHTTPErrorHandler's status derivation so the access
|
|
// log reflects the status the client actually receives — without
|
|
// this, every silent handler error logs as 200.
|
|
status := res.Status
|
|
if err != nil && !res.Committed {
|
|
status = http.StatusInternalServerError
|
|
var he *echo.HTTPError
|
|
if errors.As(err, &he) {
|
|
status = he.Code
|
|
}
|
|
}
|
|
|
|
// Fix for #7989: Reduce log verbosity of Web UI polling, resources API, and health checks
|
|
// These paths are logged at DEBUG level (hidden by default) instead of INFO.
|
|
isQuietPath := false
|
|
for _, path := range quietPaths {
|
|
if req.URL.Path == path {
|
|
isQuietPath = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// URL.Path never holds the query string, so keys passed as
|
|
// ?api_key=... stay out of the log.
|
|
attrs := []any{"method", req.Method, "path", req.URL.Path, "status", status}
|
|
if logClient {
|
|
attrs = append(attrs, "duration", time.Since(start).String(), "remote", peerAddr(req), "user_agent", sanitizeLogValue(req.UserAgent(), maxLoggedUserAgent))
|
|
if id := requestID(req, res); id != "" {
|
|
attrs = append(attrs, "request_id", id)
|
|
}
|
|
}
|
|
|
|
if isQuietPath && status == 200 {
|
|
xlog.Debug("HTTP request", attrs...)
|
|
} else {
|
|
xlog.Info("HTTP request", attrs...)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// peerAddr returns the TCP peer address without the port. It does not use
|
|
// echo's Context.RealIP: with no IPExtractor configured, RealIP trusts the
|
|
// X-Forwarded-For and X-Real-Ip headers, which any client can forge. LocalAI
|
|
// has no trusted-proxy setting, so the peer address is the only safe value.
|
|
func peerAddr(req *http.Request) string {
|
|
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
|
if err != nil {
|
|
return req.RemoteAddr
|
|
}
|
|
return host
|
|
}
|
|
|
|
// requestID returns the diagnostic id the server set on the response, or the
|
|
// X-Request-Id the caller sent, or an empty string.
|
|
func requestID(req *http.Request, res *echo.Response) string {
|
|
id := res.Header().Get(diagnostics.HeaderDiagnosticID)
|
|
if id == "" {
|
|
id = req.Header.Get(echo.HeaderXRequestID)
|
|
}
|
|
return sanitizeLogValue(id, maxLoggedRequestID)
|
|
}
|
|
|
|
// sanitizeLogValue replaces control characters with a space and truncates
|
|
// the value to max characters.
|
|
func sanitizeLogValue(ua string, max int) string {
|
|
var b strings.Builder
|
|
n := 0
|
|
for _, r := range ua {
|
|
if n == max {
|
|
break
|
|
}
|
|
if unicode.IsControl(r) {
|
|
r = ' '
|
|
}
|
|
b.WriteRune(r)
|
|
n++
|
|
}
|
|
return b.String()
|
|
}
|