Files
NianAIGC/backend/internal/application/event_logging.go

159 lines
5.3 KiB
Go

package application
import (
"context"
"io"
"net/http"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/jobs"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/logging"
)
// EventLogger is the application-owned write seam implemented by
// *logging.Service. Keeping the seam here lets runtime composition provide a
// different sink without coupling either HTTP handlers or jobs to log storage.
type EventLogger interface {
Append(context.Context, logging.Input) (logging.Entry, error)
}
// WithHTTPEventLogging records server failures while deliberately limiting
// request metadata to the method and URL path. Headers, cookies, query values,
// and bodies never enter the event.
func WithHTTPEventLogging(next http.Handler, logger EventLogger) http.Handler {
if next == nil {
next = http.NotFoundHandler()
}
return http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
writer := &eventResponseWriter{ResponseWriter: w, status: http.StatusOK}
defer func() {
if recovered := recover(); recovered != nil {
// Once a response has been streamed, HTTP cannot safely rewrite it.
// Before the first byte, however, we can still return a stable generic
// response without exposing the panic value.
if !writer.wroteHeader {
clearResponseHeaders(writer.Header())
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
}
appendEvent(request.Context(), logger, logging.Input{
Level: logging.Error, Source: "http", Message: "HTTP handler panic",
Status: http.StatusInternalServerError, Method: request.Method, Path: request.URL.Path,
})
if writer.wroteHeader && writer.status != http.StatusInternalServerError {
// The only safe response after bytes have escaped is to abort the
// connection. net/http deliberately suppresses logging for this
// sentinel while preventing a truncated response from being reused.
panic(http.ErrAbortHandler)
}
return
}
if writer.status >= http.StatusInternalServerError {
appendEvent(request.Context(), logger, logging.Input{
Level: logging.Error, Source: "http", Message: "HTTP request failed",
Status: writer.status, Method: request.Method, Path: request.URL.Path,
})
}
}()
next.ServeHTTP(writer, request)
})
}
type eventResponseWriter struct {
http.ResponseWriter
status int
wroteHeader bool
}
func (writer *eventResponseWriter) WriteHeader(status int) {
if writer.wroteHeader {
return
}
writer.status = status
writer.wroteHeader = true
writer.ResponseWriter.WriteHeader(status)
}
func (writer *eventResponseWriter) Write(body []byte) (int, error) {
if !writer.wroteHeader {
writer.WriteHeader(http.StatusOK)
}
return writer.ResponseWriter.Write(body)
}
// Unwrap lets http.ResponseController discover capabilities provided by the
// original server writer without making event logging own those interfaces.
func (writer *eventResponseWriter) Unwrap() http.ResponseWriter {
return writer.ResponseWriter
}
// ReadFrom preserves io.Copy's streaming fast path for downloads while still
// recording the implicit 200 response status.
func (writer *eventResponseWriter) ReadFrom(source io.Reader) (int64, error) {
if !writer.wroteHeader {
writer.WriteHeader(http.StatusOK)
}
if readerFrom, ok := writer.ResponseWriter.(io.ReaderFrom); ok {
return readerFrom.ReadFrom(source)
}
return io.Copy(writer.ResponseWriter, source)
}
func clearResponseHeaders(header http.Header) {
for name := range header {
delete(header, name)
}
}
type eventLoggingTickRunner struct {
next jobs.TickRunner
logger EventLogger
}
// WithTickEventLogging records both whole-tick failures and per-job failures.
// Error strings and worker/job inputs are intentionally omitted from events.
func WithTickEventLogging(next jobs.TickRunner, logger EventLogger) eventLoggingTickRunner {
return eventLoggingTickRunner{next: next, logger: logger}
}
func (runner eventLoggingTickRunner) Tick(ctx context.Context, workerID string) (jobs.TickResult, error) {
return runner.record(ctx, func() (jobs.TickResult, error) { return runner.next.Tick(ctx, workerID) })
}
// TickLimit preserves the internal Worker HTTP seam while sharing the same
// best-effort event policy as the embedded loop.
func (runner eventLoggingTickRunner) TickLimit(ctx context.Context, workerID string, limit int) (jobs.TickResult, error) {
limited, ok := runner.next.(interface {
TickLimit(context.Context, string, int) (jobs.TickResult, error)
})
if !ok {
return runner.Tick(ctx, workerID)
}
return runner.record(ctx, func() (jobs.TickResult, error) { return limited.TickLimit(ctx, workerID, limit) })
}
func (runner eventLoggingTickRunner) record(ctx context.Context, tick func() (jobs.TickResult, error)) (jobs.TickResult, error) {
result, err := tick()
if err != nil {
appendEvent(ctx, runner.logger, logging.Input{
Level: logging.Error, Source: "worker", Message: "Worker tick failed",
})
return result, err
}
for _, item := range result.Jobs {
if item.Error == "" {
continue
}
appendEvent(ctx, runner.logger, logging.Input{
Level: logging.Error, Source: "worker", Message: "Worker job failed",
})
}
return result, nil
}
func appendEvent(ctx context.Context, logger EventLogger, input logging.Input) {
if logger == nil {
return
}
defer func() { _ = recover() }()
_, _ = logger.Append(context.WithoutCancel(ctx), input)
}