145 lines
3.6 KiB
Go
145 lines
3.6 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/ollama/ollama/envconfig"
|
|
)
|
|
|
|
type inferenceRequestLogger struct {
|
|
dir string
|
|
counter uint64
|
|
}
|
|
|
|
func newInferenceRequestLogger() (*inferenceRequestLogger, error) {
|
|
dir, err := os.MkdirTemp("", "ollama-request-logs-*")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &inferenceRequestLogger{dir: dir}, nil
|
|
}
|
|
|
|
func (s *Server) initRequestLogging() error {
|
|
if !envconfig.DebugLogRequests() {
|
|
return nil
|
|
}
|
|
|
|
requestLogger, err := newInferenceRequestLogger()
|
|
if err != nil {
|
|
return fmt.Errorf("enable OLLAMA_DEBUG_LOG_REQUESTS: %w", err)
|
|
}
|
|
|
|
s.requestLogger = requestLogger
|
|
slog.Info(fmt.Sprintf("request debug logging enabled; inference request logs will be stored in %s and include request bodies and replay curl commands", requestLogger.dir))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) withInferenceRequestLogging(route string, handlers ...gin.HandlerFunc) []gin.HandlerFunc {
|
|
if s.requestLogger == nil {
|
|
return handlers
|
|
}
|
|
|
|
return append([]gin.HandlerFunc{s.requestLogger.middleware(route)}, handlers...)
|
|
}
|
|
|
|
func (l *inferenceRequestLogger) middleware(route string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if c.Request == nil {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
method := c.Request.Method
|
|
host := c.Request.Host
|
|
scheme := "http"
|
|
if c.Request.TLS != nil {
|
|
scheme = "https"
|
|
}
|
|
contentType := c.GetHeader("Content-Type")
|
|
|
|
var body []byte
|
|
if c.Request.Body != nil {
|
|
var err error
|
|
body, err = io.ReadAll(c.Request.Body)
|
|
c.Request.Body = io.NopCloser(bytes.NewReader(body))
|
|
if err != nil {
|
|
slog.Warn("failed to read request body for debug logging", "route", route, "error", err)
|
|
}
|
|
}
|
|
|
|
c.Next()
|
|
l.log(route, method, scheme, host, contentType, body)
|
|
}
|
|
}
|
|
|
|
func (l *inferenceRequestLogger) log(route, method, scheme, host, contentType string, body []byte) {
|
|
if l == nil || l.dir == "" {
|
|
return
|
|
}
|
|
|
|
if contentType == "" {
|
|
contentType = "application/json"
|
|
}
|
|
if host == "" || scheme == "" {
|
|
base := envconfig.Host()
|
|
if host == "" {
|
|
host = base.Host
|
|
}
|
|
if scheme == "" {
|
|
scheme = base.Scheme
|
|
}
|
|
}
|
|
|
|
routeForFilename := sanitizeRouteForFilename(route)
|
|
timestamp := fmt.Sprintf("%s-%06d", time.Now().UTC().Format("20060102T150405.000000000Z"), atomic.AddUint64(&l.counter, 1))
|
|
bodyFilename := fmt.Sprintf("%s_%s_body.json", timestamp, routeForFilename)
|
|
curlFilename := fmt.Sprintf("%s_%s_request.sh", timestamp, routeForFilename)
|
|
bodyPath := filepath.Join(l.dir, bodyFilename)
|
|
curlPath := filepath.Join(l.dir, curlFilename)
|
|
|
|
if err := os.WriteFile(bodyPath, body, 0o600); err != nil {
|
|
slog.Warn("failed to write debug request body", "route", route, "error", err)
|
|
return
|
|
}
|
|
|
|
url := fmt.Sprintf("%s://%s%s", scheme, host, route)
|
|
curl := fmt.Sprintf("#!/bin/sh\nSCRIPT_DIR=\"$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\"\ncurl --request %s --url %q --header %q --data-binary @\"${SCRIPT_DIR}/%s\"\n", method, url, "Content-Type: "+contentType, bodyFilename)
|
|
if err := os.WriteFile(curlPath, []byte(curl), 0o600); err != nil {
|
|
slog.Warn("failed to write debug request replay command", "route", route, "error", err)
|
|
return
|
|
}
|
|
|
|
slog.Info(fmt.Sprintf("logged to %s, replay using curl with `sh %s`", bodyPath, curlPath))
|
|
}
|
|
|
|
func sanitizeRouteForFilename(route string) string {
|
|
route = strings.TrimPrefix(route, "/")
|
|
if route == "" {
|
|
return "root"
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.Grow(len(route))
|
|
for _, r := range route {
|
|
if ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z') || ('0' <= r && r <= '9') {
|
|
b.WriteRune(r)
|
|
} else {
|
|
b.WriteByte('_')
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|