teg/backend/user_log.go
2026-05-22 14:11:18 +02:00

401 lines
7.0 KiB
Go

// backend/user_log.go
package main
import (
"context"
"encoding/json"
"net"
"net/http"
"strings"
)
type UserLogLevel string
const (
UserLogLevelInfo UserLogLevel = "info"
UserLogLevelWarning UserLogLevel = "warning"
UserLogLevelError UserLogLevel = "error"
)
const (
maxUserLogEntriesPerUser = 500
maxAnonymousUserLogEntries = 1000
)
type UserLogEntry struct {
User *User
UserID string
Request *http.Request
Level UserLogLevel
Category string
Action string
Message string
Error error
Details LogFields
}
func (s *Server) logUserEvent(ctx context.Context, entry UserLogEntry) {
if entry.Level == "" {
entry.Level = UserLogLevelInfo
}
userID := any(nil)
userIDForPrune := ""
userSnapshot := map[string]any{}
if entry.User != nil {
userID = entry.User.ID
userIDForPrune = entry.User.ID
userSnapshot = buildUserLogSnapshot(*entry.User)
} else if strings.TrimSpace(entry.UserID) != "" {
userID = strings.TrimSpace(entry.UserID)
userIDForPrune = strings.TrimSpace(entry.UserID)
}
requestSnapshot := map[string]any{}
if entry.Request != nil {
requestSnapshot = buildRequestLogSnapshot(entry.Request)
}
details := mergeLogFields(appErrorFields(entry.Error), entry.Details)
errorMessage := ""
if entry.Error != nil {
errorMessage = entry.Error.Error()
}
userSnapshotJSON, err := json.Marshal(userSnapshot)
if err != nil {
logError("User-Log konnte nicht serialisiert werden", err, nil)
return
}
requestSnapshotJSON, err := json.Marshal(requestSnapshot)
if err != nil {
logError("Request-Log konnte nicht serialisiert werden", err, nil)
return
}
detailsJSON, err := json.Marshal(details)
if err != nil {
logError("User-Log-Details konnten nicht serialisiert werden", err, nil)
return
}
_, err = s.db.Exec(
ctx,
`
INSERT INTO user_logs (
user_id,
level,
category,
action,
message,
error,
user_snapshot,
request,
details
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`,
userID,
string(entry.Level),
entry.Category,
entry.Action,
entry.Message,
errorMessage,
userSnapshotJSON,
requestSnapshotJSON,
detailsJSON,
)
if err != nil {
logError("User-Log konnte nicht gespeichert werden", err, LogFields{
"category": entry.Category,
"action": entry.Action,
})
return
}
if userIDForPrune != "" {
s.pruneUserLogsByUserID(ctx, userIDForPrune)
return
}
s.pruneAnonymousUserLogs(ctx)
}
func (s *Server) pruneUserLogs(ctx context.Context, user *User) {
if user == nil || user.ID == "" {
return
}
s.pruneUserLogsByUserID(ctx, user.ID)
}
func (s *Server) pruneUserLogsByUserID(ctx context.Context, userID string) {
userID = strings.TrimSpace(userID)
if userID == "" {
return
}
_, err := s.db.Exec(
ctx,
`
DELETE FROM user_logs
WHERE user_id = $1
AND id NOT IN (
SELECT id
FROM user_logs
WHERE user_id = $1
ORDER BY created_at DESC, id DESC
LIMIT $2
)
`,
userID,
maxUserLogEntriesPerUser,
)
if err != nil {
logError("Alte User-Logs konnten nicht gelöscht werden", err, LogFields{
"userId": userID,
"limit": maxUserLogEntriesPerUser,
})
}
}
func (s *Server) pruneAnonymousUserLogs(ctx context.Context) {
_, err := s.db.Exec(
ctx,
`
DELETE FROM user_logs
WHERE user_id IS NULL
AND id NOT IN (
SELECT id
FROM user_logs
WHERE user_id IS NULL
ORDER BY created_at DESC, id DESC
LIMIT $1
)
`,
maxAnonymousUserLogEntries,
)
if err != nil {
logError("Alte anonyme User-Logs konnten nicht gelöscht werden", err, LogFields{
"limit": maxAnonymousUserLogEntries,
})
}
}
func (s *Server) logUserError(
r *http.Request,
user User,
category string,
action string,
message string,
err error,
details LogFields,
) {
s.logUserEvent(r.Context(), UserLogEntry{
User: &user,
Request: r,
Level: UserLogLevelError,
Category: category,
Action: action,
Message: message,
Error: err,
Details: details,
})
}
func (s *Server) logAndWriteUserError(
w http.ResponseWriter,
r *http.Request,
user User,
statusCode int,
category string,
action string,
publicMessage string,
err error,
details LogFields,
) {
s.logUserError(
r,
user,
category,
action,
publicMessage,
err,
details,
)
writeError(w, statusCode, publicMessage)
}
func buildUserLogSnapshot(user User) map[string]any {
return map[string]any{
"id": user.ID,
"username": user.Username,
"displayName": user.DisplayName,
"email": user.Email,
"avatar": user.Avatar,
"unit": user.Unit,
"group": user.Group,
"rights": user.Rights,
"teams": user.Teams,
}
}
func buildRequestLogSnapshot(r *http.Request) map[string]any {
if r == nil {
return map[string]any{}
}
return map[string]any{
"method": r.Method,
"path": r.URL.Path,
"query": r.URL.RawQuery,
"ipAddress": requestIPAddress(r),
"remoteAddr": r.RemoteAddr,
"userAgent": r.UserAgent(),
"referer": r.Referer(),
}
}
func requestIPAddress(r *http.Request) string {
for _, headerName := range []string{
"X-Forwarded-For",
"X-Real-IP",
"CF-Connecting-IP",
} {
headerValue := strings.TrimSpace(r.Header.Get(headerName))
if headerValue == "" {
continue
}
ipAddress := strings.TrimSpace(strings.Split(headerValue, ",")[0])
if ipAddress != "" {
return ipAddress
}
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
return host
}
return r.RemoteAddr
}
func (s *Server) logRequestEvent(
r *http.Request,
level UserLogLevel,
category string,
action string,
message string,
err error,
details LogFields,
) {
var user *User
userID := ""
if currentUser, ok := userFromContext(r.Context()); ok {
user = &currentUser
} else if currentUserID, ok := s.currentUserIDFromRequest(r); ok {
userID = currentUserID
}
s.logUserEvent(r.Context(), UserLogEntry{
User: user,
UserID: userID,
Request: r,
Level: level,
Category: category,
Action: action,
Message: message,
Error: err,
Details: details,
})
}
func (s *Server) logRequestInfo(
r *http.Request,
category string,
action string,
message string,
details LogFields,
) {
s.logRequestEvent(
r,
UserLogLevelInfo,
category,
action,
message,
nil,
details,
)
}
func (s *Server) logRequestWarning(
r *http.Request,
category string,
action string,
message string,
err error,
details LogFields,
) {
s.logRequestEvent(
r,
UserLogLevelWarning,
category,
action,
message,
err,
details,
)
}
func (s *Server) logRequestError(
r *http.Request,
category string,
action string,
message string,
err error,
details LogFields,
) {
s.logRequestEvent(
r,
UserLogLevelError,
category,
action,
message,
err,
details,
)
}
func (s *Server) logAndWriteSettingsError(
w http.ResponseWriter,
r *http.Request,
statusCode int,
category string,
action string,
publicMessage string,
err error,
details LogFields,
) {
s.logRequestError(
r,
category,
action,
publicMessage,
err,
details,
)
writeSettingsError(w, statusCode, publicMessage)
}