Files
summercms/modules/pier/audit.go
Jakub Zych 2bf9dca603 fix(pier): reap stateful sessions whose store record expired
- a reaper started at Mount checks every local session against the store
  every min(idle_ttl/2, 1m) and closes each copy whose record is gone,
  logging the expired step (A-CR-01, A-IN-06)
- the GET stream re-check ends the stream when the record is gone (A-IN-01)
- tool calls served without a session on the stateful path log the
  session as ephemeral (A-IN-06)
- TestPierReaperSmoke and the ReapNow, LocalSessions, StopReaper seams
2026-10-23 15:50:04 +02:00

79 lines
2.3 KiB
Go

package pier
import (
"log/slog"
"time"
"git.golem15.com/golem15/summercms/modules/diary"
)
// LogComponent is the diary component pier logs under.
const LogComponent = "pier"
// sessionPrefixLen is how much of a session id the audit lines show.
const sessionPrefixLen = 8
// stateless is the session value of a call served on the stateless path.
const stateless = "stateless"
// logger is the module's logger (component pier).
func logger() *slog.Logger {
return diary.For(LogComponent)
}
// sessionPrefix shortens a session id for logging; a stateless call logs
// "stateless" and a call served without a session on the stateful path
// "ephemeral".
func sessionPrefix(id string) string {
if id == "" {
return stateless
}
if id == ephemeralSession {
return ephemeralSession
}
if len(id) > sessionPrefixLen {
return id[:sessionPrefixLen]
}
return id
}
// logToolCall writes the one audit line per tool call (D-23): the tool,
// its guard, the frontend user id (0 when none), the backend admin id (0
// when none), the OAuth client id, the token kind, the session prefix, the
// route's status and the duration. Never the arguments, the body or a token.
func logToolCall(log *slog.Logger, id Identity, t *tool, session string, status int, took time.Duration) {
var userID, adminID uint
var clientID string
if id.Frontend != nil {
userID = id.Frontend.UserID
clientID = id.Frontend.ClientID
}
if id.Backend != nil {
adminID = id.Backend.UserID
if clientID == "" {
clientID = id.Backend.ClientID
}
}
log.Info("pier: tool call",
"tool", t.name,
"guard", t.guardName,
"user_id", userID,
"admin_id", adminID,
"client_id", clientID,
"token_kind", id.TokenKind,
"session", sessionPrefix(session),
"status", status,
"duration_ms", took.Milliseconds(),
)
}
// logSession writes one line per session lifecycle step with the session
// prefix and the binding, never the token. The steps: open (serveNew),
// rehydrate (lookup), end (DELETE), binding_refused (another binding
// presented the id), revoked (a revocation or deletion event),
// stream_closed (a GET stream whose bearer or record is gone) and expired
// (the reaper closed a local copy whose record expired).
func logSession(log *slog.Logger, step, session, binding string) {
log.Info("pier: session", "step", step, "session", sessionPrefix(session), "binding", binding)
}