diff --git a/authn/resolver.go b/authn/resolver.go
index 22bc886e46846d37bedd0a6fb6b8d428c2fca194..210b9af647fa2adacddd0a16daa4b0bc20a439aa 100644
--- a/authn/resolver.go
+++ b/authn/resolver.go
@@ -3,9 +3,11 @@
import (
"context"
"fmt"
- "log"
+ "log/slog"
"net/http"
"strings"
+
+ "go.bigb.es/auxilia/scribe"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
)
@@ -177,7 +179,9 @@ // Fail closed and loudly. The alternative — degrading to
// anonymous — would turn a Postgres blip or an unreachable
// tokens.sr.ht into agents silently losing their write
// access.
- log.Printf("authn: resolving bearer credential: %v", err)
+ slog.ErrorContext(r.Context(), "resolving an agent credential failed",
+ "method", r.Method, "path", r.URL.Path, "status", status,
+ scribe.Err(err))
}
http.Error(w, resolveFailureMessage(status), status)
return
diff --git a/cmd/specsrht/main.go b/cmd/specsrht/main.go
index cdb6e49d81e918bfcc0dc75967dead01a1c08390..de57cc505efaa4879dff226368d4f313219be230 100644
--- a/cmd/specsrht/main.go
+++ b/cmd/specsrht/main.go
@@ -233,12 +233,41 @@
// newLogger builds the process logger. LOG_LEVEL raises or lowers verbosity;
// everything goes to stderr, because a hook's stdout is forwarded to the
// pushing client and this binary is both programs.
+//
+// It is the whole logging configuration of this service, and it is installed
+// with slog.SetDefault rather than threaded everywhere. That is what makes the
+// library packages loggable at all: the read plane, the credential resolver and
+// sr-ht-ecore's panic middleware all log through the default logger, and none
+// of them takes a *slog.Logger — a middleware in another module cannot be
+// handed this one, and without the SetDefault its panic reports would come out
+// of Go's plain stderr handler with none of this applied.
+//
+// The source position is on because most of what reaches this handler is a
+// failure, and "which of the six render sites" is the first thing anybody asks.
+// Colour is dropped when stderr is not a terminal, so the journal does not
+// collect escape sequences.
+//
+// The masks are the reason to configure this in one place at all. This daemon
+// handles the unified-login cookie and tokens.sr.ht working tokens, and a
+// wrapped error or a struct logged whole is how a live credential reaches a log
+// file — where it outlives the request, the process and usually the token's own
+// lifetime. Masking is applied by the handler, so it holds for a log line
+// nobody reviewed as well as for the ones here.
func newLogger() *slog.Logger {
level := new(slog.LevelVar)
level.Set(parseLevel(os.Getenv("LOG_LEVEL")))
+
+ stat, err := os.Stderr.Stat()
+ noColor := err != nil || stat.Mode()&os.ModeCharDevice == 0
+
return slog.New(scribe.NewTintHandler(
scribe.WithWriter(os.Stderr),
scribe.WithLevel(level),
+ scribe.WithSource(true),
+ scribe.WithTimeFormat(time.DateTime),
+ scribe.WithNoColor(noColor),
+ scribe.WithMaskKeys("token", "cookie", "authorization", "network-key", "private-key"),
+ scribe.WithMask(`(?i)(secret|token|api_?key|password)`, "***"),
))
}
diff --git a/web/diff.go b/web/diff.go
index 796d9987272f3a61d658c8f524f286d1e5ca9b5f..50dd78f7bec68ba324fd27183daf9557e910cbf9 100644
--- a/web/diff.go
+++ b/web/diff.go
@@ -6,8 +6,10 @@ "crypto/sha256"
"encoding/hex"
"fmt"
"html/template"
- "log"
+ "log/slog"
"strings"
+
+ "go.bigb.es/auxilia/scribe"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
"sourcecraft.dev/bigbes/sr-ht-spec/prosediff"
@@ -343,7 +345,8 @@ }
var buf bytes.Buffer
if err := blockThreadsTmpl.Execute(&buf, data); err != nil {
- log.Printf("web: rendering comments on %s: %v", r.in.Path, err)
+ slog.Error("rendering the comments on a diff block failed",
+ "doc", r.in.Path, scribe.Err(err))
return
}
b.WriteString(`
= 500 {
- log.Printf("web: %s: %v", r.URL.Path, err)
+ slog.ErrorContext(r.Context(), "the read plane could not answer a request",
+ "method", r.Method, "path", r.URL.Path, "status", status, scribe.Err(err))
s.renderError(w, r, status, "")
return
}
@@ -165,7 +167,8 @@ return
}
status := httpStatusFor(err)
if status >= 500 {
- log.Printf("web: %s: %v", r.URL.Path, err)
+ slog.ErrorContext(r.Context(), "the read plane could not answer a machine request",
+ "method", r.Method, "path", r.URL.Path, "status", status, scribe.Err(err))
http.Error(w, "internal server error", status)
return
}
@@ -216,7 +219,8 @@ }
}
vd.Data = data
if err := s.pages.Render(w, http.StatusOK, "index", vd); err != nil {
- log.Printf("web: render the landing page for %s: %v", r.URL.Path, err)
+ slog.ErrorContext(r.Context(), "rendering a page failed after it was answered",
+ "page", "index", "path", r.URL.Path, scribe.Err(err))
}
}
@@ -270,7 +274,8 @@ Count: len(snap.Archive.All()),
Items: flattenTree(snap, revQuery(rev)),
}
if err := s.pages.Render(w, http.StatusOK, "space", vd); err != nil {
- log.Printf("web: render the space page for %s: %v", r.URL.Path, err)
+ slog.ErrorContext(r.Context(), "rendering a page failed after it was answered",
+ "page", "space", "path", r.URL.Path, scribe.Err(err))
}
}
@@ -485,7 +490,8 @@ w.Header().Set("Content-Type", "application/json; charset=utf-8")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(payload); err != nil {
- log.Printf("web: encoding %s.json: %v", docPath, err)
+ slog.ErrorContext(r.Context(), "encoding a document as JSON failed mid-response",
+ "doc", docPath, scribe.Err(err))
}
return
}
@@ -530,7 +536,8 @@
vd := s.view(r, page.Title+" — "+ref.String())
vd.Data = data
if err := s.pages.Render(w, http.StatusOK, "document", vd); err != nil {
- log.Printf("web: render the document page for %s: %v", r.URL.Path, err)
+ slog.ErrorContext(r.Context(), "rendering a page failed after it was answered",
+ "page", "document", "path", r.URL.Path, scribe.Err(err))
}
}
@@ -647,7 +654,8 @@ vd := s.view(r, "")
vd.Title = "search — " + vd.SiteName + " " + vd.SiteLabel
vd.Data = data
if err := s.pages.Render(w, http.StatusOK, "search", vd); err != nil {
- log.Printf("web: render the search page for %s: %v", r.URL.Path, err)
+ slog.ErrorContext(r.Context(), "rendering a page failed after it was answered",
+ "page", "search", "path", r.URL.Path, scribe.Err(err))
}
}
diff --git a/web/inbox.go b/web/inbox.go
index f67a387522dae6e95bdd4d8ee35bc84a8d3242cf..d953f7c041a45c6f8c3bf4a41f7fe7302533349e 100644
--- a/web/inbox.go
+++ b/web/inbox.go
@@ -1,10 +1,12 @@
package web
import (
- "log"
+ "log/slog"
"net/http"
"strconv"
"time"
+
+ "go.bigb.es/auxilia/scribe"
"sourcecraft.dev/bigbes/sr-ht-spec/authn"
"sourcecraft.dev/bigbes/sr-ht-spec/service"
@@ -72,7 +74,8 @@ Digest: digestRows,
NewCount: newCount,
}
if err := s.pages.Render(w, http.StatusOK, "inbox", vd); err != nil {
- log.Printf("web: render the inbox page for %s: %v", r.URL.Path, err)
+ slog.ErrorContext(r.Context(), "rendering a page failed after it was answered",
+ "page", "inbox", "path", r.URL.Path, scribe.Err(err))
}
}
diff --git a/web/proposal.go b/web/proposal.go
index 625a0f7ea014ddc8e159ec1a35c2e6c26e87cdfe..9ec107eaa6c7ae94da4c42164d32c167a8037fc6 100644
--- a/web/proposal.go
+++ b/web/proposal.go
@@ -3,11 +3,12 @@
import (
"context"
"fmt"
- "log"
+ "log/slog"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
+ "go.bigb.es/auxilia/scribe"
"sourcecraft.dev/bigbes/sr-ht-spec/authn"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
@@ -172,7 +173,8 @@ Lost: lostPanels(lost, controls),
Unresolved: unresolvedThreads(threads),
}
if err := s.pages.Render(w, http.StatusOK, "proposal", vd); err != nil {
- log.Printf("web: render the proposal page for %s: %v", r.URL.Path, err)
+ slog.ErrorContext(r.Context(), "rendering a page failed after it was answered",
+ "page", "proposal", "proposal_id", p.ID, "path", r.URL.Path, scribe.Err(err))
}
}
diff --git a/web/server.go b/web/server.go
index 3676829236a629199116ce70a705ccd5af4476ad..af0ba42e45dba809b9b0b8b3794a40938f8efde3 100644
--- a/web/server.go
+++ b/web/server.go
@@ -83,7 +83,7 @@
import (
"fmt"
"io/fs"
- "log"
+ "log/slog"
"net/http"
"github.com/vaughan0/go-ini"
@@ -199,8 +199,8 @@ if err != nil {
return nil, fmt.Errorf("web: %w", err)
}
if cssHref == "" {
- log.Printf("web: no main.min.*.css embedded in this binary — pages will " +
- "render unstyled; run `make css` before `go build`")
+ slog.Warn("no stylesheet is embedded in this binary, so pages will render unstyled",
+ "glob", "static/main.min.*.css", "remedy", "run `make css` before `go build`")
}
// chrome.Page renders a bare page for an empty StyleHref rather than an
// empty , so an unstyled build stays a presentation failure.
diff --git a/web/templates.go b/web/templates.go
index 9ef8bbd911d06ea4bad7d8eeb4d5e4fa2434c956..eff6a47c46ba8061810393996546fb76c308eb06 100644
--- a/web/templates.go
+++ b/web/templates.go
@@ -3,10 +3,11 @@
import (
"embed"
"html/template"
- "log"
+ "log/slog"
"net/http"
"strings"
+ "go.bigb.es/auxilia/scribe"
"sourcecraft.dev/bigbes/sr-ht-ecore/chrome"
"sourcecraft.dev/bigbes/sr-ht-ecore/pages"
)
@@ -83,6 +84,7 @@ func (s *Server) renderError(w http.ResponseWriter, r *http.Request, status int, message string) {
vd := s.view(r, http.StatusText(status))
vd.Data = pages.Error(status, message)
if err := s.pages.Render(w, status, pages.ErrorPage, vd); err != nil {
- log.Printf("web: render the %d page for %s %s: %v", status, r.Method, r.URL.Path, err)
+ slog.ErrorContext(r.Context(), "rendering the error page failed after it was answered",
+ "status", status, "method", r.Method, "path", r.URL.Path, scribe.Err(err))
}
}