M authn/cookie.go => authn/cookie.go +5 -2
@@ 2,9 2,11 @@ package authn
import (
"encoding/json"
- "log"
+ "log/slog"
"net/http"
+ "go.bigb.es/auxilia/scribe"
+
"sourcecraft.dev/bigbes/sr-ht-core/auth"
"sourcecraft.dev/bigbes/sr-ht-core/crypto"
)
@@ 67,7 69,8 @@ func resolveCookie(r *http.Request) *auth.AuthContext {
if err := meta.LookupUser(r.Context(), authCookie.Name, &ac); err != nil {
// meta/database unreachable or unknown user: degrade to anonymous
// rather than failing the request (browsing must keep working).
- log.Printf("authn: cookie LookupUser(%q): %v", authCookie.Name, err)
+ slog.WarnContext(r.Context(), "resolving the login cookie's user failed",
+ "component", "authn", "username", authCookie.Name, scribe.Err(err))
return nil
}
ac.AuthMethod = auth.AUTH_COOKIE
M authn/jwt.go => authn/jwt.go +7 -3
@@ 5,13 5,16 @@ import (
"crypto/ed25519"
"crypto/subtle"
"fmt"
- "log"
+ "log/slog"
"time"
- "sourcecraft.dev/bigbes/sr-ht-core/auth"
"github.com/dolthub/dolt/go/libraries/doltcore/creds"
jose "gopkg.in/go-jose/go-jose.v2"
"gopkg.in/go-jose/go-jose.v2/jwt"
+
+ "go.bigb.es/auxilia/scribe"
+
+ "sourcecraft.dev/bigbes/sr-ht-core/auth"
)
// jwtSubjectPrefix is the fixed prefix dolt puts in the JWT "sub" claim,
@@ 110,7 113,8 @@ func ResolveDoltJWT(ctx context.Context, rawJWT, expectedAud string, keys KeySto
if err := keys.TouchLastUsed(ctx, kid); err != nil {
// Non-fatal: the caller is already authenticated; last_used is display
// metadata. Log and continue rather than failing the clone/push.
- log.Printf("authn: TouchLastUsed(%q): %v", kid, err)
+ slog.WarnContext(ctx, "recording the last use of a dolt key failed",
+ "component", "authn", "kid", kid, scribe.Err(err))
}
return &ac, nil
}
A cmd/doltsrht/logging.go => cmd/doltsrht/logging.go +60 -0
@@ 0,0 1,60 @@
+package main
+
+import (
+ "log/slog"
+ "os"
+ "time"
+
+ "github.com/vaughan0/go-ini"
+
+ "go.bigb.es/auxilia/scribe"
+
+ "sourcecraft.dev/bigbes/sr-ht-core/config"
+)
+
+// setupLogging installs the process-wide slog handler.
+//
+// It sets the *default* logger rather than building one to thread through
+// constructors, and that is the point rather than a shortcut. sr-ht-ecore's
+// middleware reports a recovered panic through slog's default; so does every
+// library package in this service that has no constructor to be handed a logger
+// through (authn's cookie and JWT resolvers, the web renderer). A daemon that
+// skipped this call would still log all of it — into Go's plain stderr handler,
+// without a level, without source positions, and above all without the masking
+// below.
+//
+// The level comes from [dolt.sr.ht]log-level ("debug", "info", "warn",
+// "error"); an unreadable value is info, because a daemon that refused to boot
+// over a typo in a log level would be trading an operator's whole service for
+// their logging preference.
+func setupLogging(conf ini.File) {
+ var level slog.Level
+ if err := level.UnmarshalText([]byte(config.GetString(conf, serviceName, "log-level", "info"))); err != nil {
+ level = slog.LevelInfo
+ }
+
+ slog.SetDefault(slog.New(scribe.NewTintHandler(
+ scribe.WithWriter(os.Stderr),
+ scribe.WithLevel(level),
+ scribe.WithSource(true),
+ scribe.WithTimeFormat(time.DateTime),
+ // Colour is for a terminal; under systemd or a container's log
+ // collector the escapes are noise in the journal.
+ scribe.WithNoColor(!isTerminal(os.Stderr)),
+ // The masks are keyed on the attribute *path*, not on the message, so
+ // they cost nothing in prose and cannot be defeated by a sentence that
+ // happens to contain the word "token". These three are the credentials
+ // this service handles: the unified-login cookie, the "Internal"
+ // service-to-service fernet token and the Authorization header the
+ // remotesapi interceptors read a PAT or a keypair JWT out of.
+ scribe.WithMaskKeys("token", "cookie", "authorization"),
+ scribe.WithMask(`(?i)(secret|token|api_?key|password|pubkey|credential)`, "***"),
+ )))
+}
+
+// isTerminal reports whether f is a character device, which is the whole of
+// what the colour decision needs and does not require a dependency to answer.
+func isTerminal(f *os.File) bool {
+ info, err := f.Stat()
+ return err == nil && info.Mode()&os.ModeCharDevice != 0
+}
M cmd/doltsrht/main.go => cmd/doltsrht/main.go +38 -17
@@ 21,16 21,18 @@ import (
"context"
"database/sql"
"fmt"
- "log"
+ "log/slog"
"net/url"
"os"
"github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware"
_ "github.com/lib/pq" // registers the "postgres" database/sql driver
- "github.com/sirupsen/logrus"
"github.com/vaughan0/go-ini"
+ "go.bigb.es/auxilia/culpa"
+ "go.bigb.es/auxilia/scribe"
+
"sourcecraft.dev/bigbes/sr-ht-core/config"
"sourcecraft.dev/bigbes/sr-ht-core/database"
"sourcecraft.dev/bigbes/sr-ht-core/server"
@@ 94,12 96,19 @@ type settings struct {
func resolveSettings(conf ini.File) (settings, error) {
connString, ok := conf.Get(serviceName, "connection-string")
if !ok || connString == "" {
- return settings{}, fmt.Errorf("missing required [%s]connection-string in config.ini", serviceName)
+ // A hint rather than a longer sentence: scribe prints it on its own
+ // line, and what an operator meeting this needs is the key to add, not
+ // a restatement of the failure.
+ return settings{}, culpa.WithHint(
+ culpa.New("no connection-string is configured"),
+ "set [dolt.sr.ht]connection-string in config.ini")
}
host, err := hostFromOrigin(config.GetOrigin(conf, serviceName, true))
if err != nil {
- return settings{}, fmt.Errorf("[%s]origin: %w", serviceName, err)
+ return settings{}, culpa.WithHint(
+ culpa.Wrapf(err, "[%s]origin", serviceName),
+ "origin must be protocol://host; it is what places this service in every other service's nav")
}
return settings{
@@ 134,6 143,10 @@ func hostFromOrigin(origin string) (string, error) {
func main() {
conf := config.LoadConfig()
+ // Before anything that can fail: everything below, and every library this
+ // process links, reports through slog's default logger.
+ setupLogging(conf)
+
// server.New parses -b/-d/-m/-p and runs crypto.InitCrypto (needs
// [sr.ht]network-key + [webhooks]private-key; missing keys panic here).
// Pass the full os.Args: core-go's getopt skips argv[0] as the program name
@@ 145,16 158,14 @@ func main() {
cfg, err := resolveSettings(conf)
if err != nil {
- log.Fatalf("doltsrht: %v", err)
+ fatal("reading the configuration", err)
}
db, err := sql.Open("postgres", cfg.connString)
if err != nil {
- log.Fatalf("doltsrht: open postgres: %v", err)
+ fatal("opening the postgres pool", err)
}
- logger := logrus.NewEntry(logrus.StandardLogger())
-
// Build the remotesapi server first: its chunk-store cache backs the web
// store manager's Evict.
rapiConf := remoteapi.Config{
@@ 164,15 175,14 @@ func main() {
ListenAddr: cfg.remotesapiAddr,
CredsListenAddr: cfg.credsapiAddr,
HttpHost: cfg.httpHost,
- Logger: logger,
}
rsrv, err := remoteapi.New(rapiConf)
if err != nil {
- log.Fatalf("doltsrht: build remotesapi server: %v", err)
+ fatal("building the remotesapi server", err)
}
csrv, err := remoteapi.NewCredServer(rapiConf)
if err != nil {
- log.Fatalf("doltsrht: build credentials server: %v", err)
+ fatal("building the credentials server", err)
}
stores := &storeManager{cache: rsrv.Cache()}
@@ 195,7 205,7 @@ func main() {
return storage.RepoDiskPath(cfg.reposRoot, owner, name)
},
}); err != nil {
- log.Fatalf("doltsrht: web.Register: %v", err)
+ fatal("mounting the web routes", err)
}
})
@@ 203,24 213,35 @@ func main() {
// goroutines and let the web server's Run own the SIGINT lifecycle.
go func() {
if err := rsrv.Serve(); err != nil {
- log.Fatalf("doltsrht: remotesapi serve: %v", err)
+ fatal("serving the remotesapi", err)
}
}()
go func() {
if err := csrv.Serve(); err != nil {
- log.Fatalf("doltsrht: credentials serve: %v", err)
+ fatal("serving the credentials api", err)
}
}()
- logger.Infof("doltsrht: remotesapi on %s, credentials on %s, web on %s",
- cfg.remotesapiAddr, cfg.credsapiAddr, defaultWebAddr)
+ slog.Info("listening",
+ "remotesapi", cfg.remotesapiAddr,
+ "credentials", cfg.credsapiAddr,
+ "web", defaultWebAddr)
// Blocks until SIGINT, then returns after draining the web listeners.
srv.Run()
// GracefulStop on the remotesapi server also closes every memoized chunk
// store (its cache Close), so no separate storage cache Close is needed.
- logger.Info("doltsrht: stopping gRPC servers")
+ slog.Info("stopping the grpc servers")
rsrv.GracefulStop()
csrv.GracefulStop()
}
+
+// fatal reports a startup failure and ends the process. slog has no Fatal, on
+// the argument that a logging call should not decide a program's lifetime; this
+// is the one place in this binary that wants both, so it is written once here
+// rather than as an Error/Exit pair at every call site.
+func fatal(doing string, err error) {
+ slog.Error(doing+" failed", scribe.Err(err))
+ os.Exit(1)
+}
M config.example.ini => config.example.ini +5 -0
@@ 38,6 38,11 @@ static-dir=/usr/share/sourcehut/dolt.sr.ht/static
;
; Set to "yes" to run brant migrations automatically on package upgrade.
migrate-on-upgrade=yes
+;
+; Verbosity of the daemon's structured log: debug, info, warn or error. An
+; unreadable value is read as info rather than refusing to boot — a typo in a
+; logging preference must not cost the service.
+log-level=info
; ---------------------------------------------------------------------------
; Shared keys reused in place (owned by other services, listed for reference;
M go.mod => go.mod +4 -3
@@ 9,9 9,10 @@ require (
github.com/fernet/fernet-go v0.0.0-20211208181803-9f70042a33ee
github.com/go-chi/chi/v5 v5.3.1
github.com/lib/pq v1.10.9
- github.com/sirupsen/logrus v1.8.3
+ github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.11.1
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec
+ go.bigb.es/auxilia v0.5.0
google.golang.org/grpc v1.79.3
gopkg.in/go-jose/go-jose.v2 v2.6.3
sourcecraft.dev/bigbes/sr-ht-core v0.0.0-20260718185800-dd418a200152
@@ 67,7 68,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.35.1 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
- github.com/cenkalti/backoff/v4 v4.1.3 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
@@ 153,7 154,7 @@ require (
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
- golang.org/x/crypto v0.51.0 // indirect
+ golang.org/x/crypto v0.52.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.20.0 // indirect
M go.sum => go.sum +8 -10
@@ 140,8 140,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
-github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
-github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ 416,8 416,8 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.8.3 h1:DBBfY8eMYazKEJHb3JKpSPfpgd2mBCoNFlQx6C5fftU=
-github.com/sirupsen/logrus v1.8.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
@@ 469,6 469,8 @@ github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
+go.bigb.es/auxilia v0.5.0 h1:S5+btW6++4CQDOfAEZe1UxrXRl6nxtmu0rI3uIXwCaQ=
+go.bigb.es/auxilia v0.5.0/go.mod h1:hBkJvydQRfmgSTR2U4PvYgJcZnh7Bj/otukrk14iJU4=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
@@ 504,8 506,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
-golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
+golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
+golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ 666,9 668,5 @@ modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sourcecraft.dev/bigbes/sr-ht-core v0.0.0-20260718185800-dd418a200152 h1:9kQC+tDO2CO8avlKadb9Z0if4a6vJuEK80+4zcb6/fU=
sourcecraft.dev/bigbes/sr-ht-core v0.0.0-20260718185800-dd418a200152/go.mod h1:Mu1Vx39ws/OTKWGoVERXvkdRSPLBdhuFTYv0ftVV31c=
-sourcecraft.dev/bigbes/sr-ht-ecore v0.0.0-20260808143603-174115990895 h1:OGZrtBtMoXhyZGXrPqMzmrNQnStoCLBVuegGo7yF1Us=
-sourcecraft.dev/bigbes/sr-ht-ecore v0.0.0-20260808143603-174115990895/go.mod h1:KeoZjm+/nnsdtc1WxB7X/0EeC+Rggt2OkwJDEc6XWnw=
-sourcecraft.dev/bigbes/sr-ht-ecore v0.0.0-20260808192241-9377c43a02ca h1:LCfxvF1VJl7djl7noAeXObfuY1csJr2OOFExwU4Y/N4=
-sourcecraft.dev/bigbes/sr-ht-ecore v0.0.0-20260808192241-9377c43a02ca/go.mod h1:KeoZjm+/nnsdtc1WxB7X/0EeC+Rggt2OkwJDEc6XWnw=
sourcecraft.dev/bigbes/sr-ht-ecore v0.0.0-20260808194355-f019dbe4ea3e h1:9yH4eagCWQMdFFbF+LQmvJjXWFdSDijhaxolrwfgatA=
sourcecraft.dev/bigbes/sr-ht-ecore v0.0.0-20260808194355-f019dbe4ea3e/go.mod h1:KeoZjm+/nnsdtc1WxB7X/0EeC+Rggt2OkwJDEc6XWnw=
M remoteapi/credsvc.go => remoteapi/credsvc.go +10 -10
@@ 5,19 5,22 @@ import (
"database/sql"
"errors"
"fmt"
+ "log/slog"
"net"
"strings"
- "sourcecraft.dev/bigbes/sr-ht-core/config"
- "sourcecraft.dev/bigbes/sr-ht-core/database"
remotesapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/remotesapi/v1alpha1"
- "github.com/sirupsen/logrus"
"github.com/vaughan0/go-ini"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
+ "go.bigb.es/auxilia/scribe"
+
+ "sourcecraft.dev/bigbes/sr-ht-core/config"
+ "sourcecraft.dev/bigbes/sr-ht-core/database"
+
"sourcecraft.dev/bigbes/sr-ht-dolt/authn"
)
@@ 34,7 37,7 @@ type credService struct {
expectedAud string
pool *sql.DB
keys authn.KeyStore
- logger *logrus.Entry
+ logger *slog.Logger
}
// WhoAmI verifies the request's Bearer keypair JWT (exactly as the remotesapi
@@ 55,7 58,7 @@ func (c *credService) WhoAmI(ctx context.Context, _ *remotesapi.WhoAmIRequest) (
if errors.Is(err, authn.ErrInvalidToken) {
return nil, status.Error(codes.Unauthenticated, "invalid or expired credentials")
}
- c.logger.Errorf("credentials WhoAmI backend error: %v", err)
+ c.logger.ErrorContext(ctx, "WhoAmI backend error", scribe.Err(err))
return nil, status.Error(codes.Unavailable, "authentication temporarily unavailable")
}
@@ 91,7 94,7 @@ func bearerToken(ctx context.Context) (string, error) {
type CredServer struct {
grpc *grpc.Server
addr string
- logger *logrus.Entry
+ logger *slog.Logger
}
// NewCredServer assembles the CredentialsService server. It shares the keystore
@@ 104,10 107,7 @@ func NewCredServer(cfg Config) (*CredServer, error) {
if cfg.CredsListenAddr == "" {
return nil, fmt.Errorf("remoteapi: NewCredServer requires a CredsListenAddr")
}
- logger := cfg.Logger
- if logger == nil {
- logger = logrus.NewEntry(logrus.StandardLogger())
- }
+ logger := slog.Default().With("component", "credentials")
svc := &credService{
conf: cfg.Conf,
M remoteapi/integration_test.go => remoteapi/integration_test.go +8 -3
@@ 133,8 133,13 @@ func TestRemoteAPIIntegration(t *testing.T) {
// (the bare host) matches what the CLI sends.
addr := freeAddr(t)
credAddr := freeAddr(t)
- logger := logrus.NewEntry(logrus.New())
- logger.Logger.SetLevel(logrus.ErrorLevel)
+
+ // dolt's remotesrv logs every chunk transfer at info; this test moves a
+ // database, so quiet it down to what a failure needs. It is a logrus entry
+ // because that is the only shape remotesrv's API accepts — see
+ // Config.DoltLogger.
+ doltLogger := logrus.NewEntry(logrus.New())
+ doltLogger.Logger.SetLevel(logrus.ErrorLevel)
cfg := Config{
Conf: conf,
@@ 143,7 148,7 @@ func TestRemoteAPIIntegration(t *testing.T) {
ListenAddr: addr,
CredsListenAddr: credAddr,
HttpHost: addr,
- Logger: logger,
+ DoltLogger: doltLogger,
}
srv, err := New(cfg)
if err != nil {
M remoteapi/interceptors.go => remoteapi/interceptors.go +25 -15
@@ 5,14 5,17 @@ import (
"database/sql"
"errors"
"fmt"
+ "log/slog"
remotesapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/remotesapi/v1alpha1"
- "github.com/sirupsen/logrus"
"github.com/vaughan0/go-ini"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
+
+ "go.bigb.es/auxilia/scribe"
+
"sourcecraft.dev/bigbes/sr-ht-core/auth"
"sourcecraft.dev/bigbes/sr-ht-core/config"
"sourcecraft.dev/bigbes/sr-ht-core/database"
@@ 87,7 90,7 @@ type interceptor struct {
service string
expectedAud string
keys authn.KeyStore
- logger *logrus.Entry
+ logger *slog.Logger
// pool is the shared database pool, threaded into the request context via
// database.Context so db.FromContext-style lookups and the token resolvers
@@ 110,13 113,14 @@ type interceptor struct {
// newInterceptor builds the interceptor over the shared pool. It binds a fresh
// db.Store per request (the pool owns connection lifetime, ctx bounds each
// query). pool and keys must be non-nil.
-func newInterceptor(conf ini.File, service, expectedAud string, pool *sql.DB, keys authn.KeyStore, reposRoot string, createStore func(ctx context.Context, absPath string) error, logger *logrus.Entry) *interceptor {
+//
+// The logger is not a parameter: it is slog's default with this component's
+// name on it. Threading one through the constructor was logrus' requirement,
+// not this package's — there is no configuration here a caller ever varied.
+func newInterceptor(conf ini.File, service, expectedAud string, pool *sql.DB, keys authn.KeyStore, reposRoot string, createStore func(ctx context.Context, absPath string) error) *interceptor {
if pool == nil {
panic("remoteapi: newInterceptor requires a non-nil *sql.DB")
}
- if logger == nil {
- logger = logrus.NewEntry(logrus.StandardLogger())
- }
if createStore == nil {
createStore = storage.InitEmptyStore
}
@@ 125,7 129,7 @@ func newInterceptor(conf ini.File, service, expectedAud string, pool *sql.DB, ke
service: service,
expectedAud: expectedAud,
keys: keys,
- logger: logger,
+ logger: slog.Default().With("component", "remotesapi.auth"),
pool: pool,
reposRoot: reposRoot,
createStore: createStore,
@@ 183,12 187,12 @@ func (i *interceptor) authenticate(ctx context.Context) (*auth.AuthContext, erro
ac, err := authn.ResolveGRPCAuth(ctx, header, i.expectedAud, i.keys)
if err != nil {
if errors.Is(err, authn.ErrInvalidToken) {
- i.logger.Warnf("remotesapi authentication rejected: %v", err)
+ i.logger.WarnContext(ctx, "authentication rejected", scribe.Err(err))
return nil, status.Error(codes.Unauthenticated, "invalid or expired credentials")
}
// Transient: meta.sr.ht or the database is unreachable. Never surface as a
// hard credential rejection — the client should retry.
- i.logger.Errorf("remotesapi authentication backend error: %v", err)
+ i.logger.ErrorContext(ctx, "authentication backend error", scribe.Err(err))
return nil, status.Error(codes.Unavailable, "authentication temporarily unavailable")
}
return ac, nil
@@ 259,7 263,8 @@ func (i *interceptor) authorize(ctx context.Context, ac *auth.AuthContext, fullM
return nil, err
}
} else {
- i.logger.Errorf("remotesapi repo lookup %s/%s: %v", owner, name, err)
+ i.logger.ErrorContext(ctx, "repository lookup failed",
+ "owner", owner, "name", name, scribe.Err(err))
return nil, status.Error(codes.Unavailable, "repository lookup temporarily unavailable")
}
}
@@ 268,7 273,8 @@ func (i *interceptor) authorize(ctx context.Context, ac *auth.AuthContext, fullM
if caller != nil {
aclMode, err = store.EffectiveAccess(ctx, caller.UserID, repo.ID)
if err != nil {
- i.logger.Errorf("remotesapi effective-access user=%d repo=%d: %v", caller.UserID, repo.ID, err)
+ i.logger.ErrorContext(ctx, "effective-access lookup failed",
+ "user_id", caller.UserID, "repo_id", repo.ID, scribe.Err(err))
return nil, status.Error(codes.Unavailable, "authorization temporarily unavailable")
}
}
@@ 305,12 311,14 @@ func (i *interceptor) autoCreate(ctx context.Context, store repoStore, caller *c
// Adopt the winner's row; do not touch disk.
repo, gerr := store.GetRepoByOwnerAndName(ctx, owner, name)
if gerr != nil {
- i.logger.Errorf("remotesapi auto-create refetch %s/%s: %v", owner, name, gerr)
+ i.logger.ErrorContext(ctx, "auto-create refetch failed",
+ "owner", owner, "name", name, scribe.Err(gerr))
return nil, status.Error(codes.Unavailable, "repository creation temporarily unavailable")
}
return repo, nil
}
- i.logger.Errorf("remotesapi auto-create %s/%s: %v", owner, name, cerr)
+ i.logger.ErrorContext(ctx, "auto-create failed",
+ "owner", owner, "name", name, scribe.Err(cerr))
return nil, status.Error(codes.Unavailable, "repository creation temporarily unavailable")
}
@@ 318,9 326,11 @@ func (i *interceptor) autoCreate(ctx context.Context, store repoStore, caller *c
// The row exists but the store does not: roll the row back (best effort)
// so the repo does not linger half-created and a retry can succeed.
if derr := store.DeleteRepo(ctx, created.ID); derr != nil {
- i.logger.Errorf("remotesapi auto-create rollback %s/%s (id=%d): %v", owner, name, created.ID, derr)
+ i.logger.ErrorContext(ctx, "auto-create rollback failed",
+ "owner", owner, "name", name, "repo_id", created.ID, scribe.Err(derr))
}
- i.logger.Errorf("remotesapi auto-create store %s/%s: %v", owner, name, serr)
+ i.logger.ErrorContext(ctx, "auto-create store initialization failed",
+ "owner", owner, "name", name, scribe.Err(serr))
return nil, status.Error(codes.Unavailable, "repository creation temporarily unavailable")
}
return created, nil
M remoteapi/interceptors_test.go => remoteapi/interceptors_test.go +11 -3
@@ 3,10 3,11 @@ package remoteapi
import (
"context"
"errors"
+ "io"
+ "log/slog"
"testing"
remotesapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/remotesapi/v1alpha1"
- "github.com/sirupsen/logrus"
"github.com/vaughan0/go-ini"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
@@ 101,12 102,19 @@ func (s *stubStore) DeleteRepo(_ context.Context, id int) error {
return nil
}
+// discardLogger is the logger these tests hand the interceptor: the paths under
+// test log on their error arms, and a test asserting a gRPC status code has no
+// use for the line.
+func discardLogger() *slog.Logger {
+ return slog.New(slog.NewTextHandler(io.Discard, nil))
+}
+
func testInterceptor(store repoStore) *interceptor {
return &interceptor{
conf: ini.File{},
service: "dolt.sr.ht",
expectedAud: "dolt.srht.bigb.es",
- logger: logrus.NewEntry(logrus.New()),
+ logger: discardLogger(),
stores: func() repoStore { return store },
reposRoot: "/tmp/repos",
createStore: func(context.Context, string) error { return nil },
@@ 449,7 457,7 @@ func TestUnaryAnonymous(t *testing.T) {
i := &interceptor{
conf: ini.File{},
service: "dolt.sr.ht",
- logger: logrus.NewEntry(logrus.New()),
+ logger: discardLogger(),
stores: func() repoStore { return &stubStore{repo: repo(1, 100, core.VisibilityPublic)} },
}
var gotCaller bool
M remoteapi/server.go => remoteapi/server.go +14 -11
@@ 4,6 4,7 @@ import (
"context"
"database/sql"
"fmt"
+ "log/slog"
"net"
remotesapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/remotesapi/v1alpha1"
@@ 12,6 13,8 @@ import (
"github.com/sirupsen/logrus"
"github.com/vaughan0/go-ini"
+ "go.bigb.es/auxilia/scribe"
+
"sourcecraft.dev/bigbes/sr-ht-dolt/db"
"sourcecraft.dev/bigbes/sr-ht-dolt/storage"
)
@@ 48,8 51,11 @@ type Config struct {
// chunk URLs (auth then cannot be host-checked; used only by tests without a
// stable host).
HttpHost string
- // Logger is the base logger; may be nil (a default is used).
- Logger *logrus.Entry
+ // DoltLogger is the logger dolt's own remotesrv writes through. It exists
+ // only because that API takes a *logrus.Entry and nothing else; everything
+ // this package logs itself goes through slog's default. nil lets remotesrv
+ // install logrus' standard logger, which is what it did before.
+ DoltLogger *logrus.Entry
}
// Server is the assembled remotesapi server: the remotesrv chunk-store server
@@ 58,7 64,7 @@ type Config struct {
type Server struct {
srv *remotesrv.Server
cache *storage.Cache
- logger *logrus.Entry
+ logger *slog.Logger
addr string
}
@@ 72,10 78,7 @@ func New(cfg Config) (*Server, error) {
if cfg.ReposRoot == "" {
return nil, fmt.Errorf("remoteapi: New requires a ReposRoot")
}
- logger := cfg.Logger
- if logger == nil {
- logger = logrus.NewEntry(logrus.StandardLogger())
- }
+ logger := slog.Default().With("component", "remotesapi")
// Repo lookup: resolve owner/name to the absolute on-disk store dir via the
// repository row. By the time Cache.Get runs, the row already exists: the
@@ 93,7 96,7 @@ func New(cfg Config) (*Server, error) {
cache := storage.NewCache(lookup)
keys := newKeyStore(cfg.DB)
- icept := newInterceptor(cfg.Conf, serviceName, normalizeAud(cfg.HttpHost), cfg.DB, keys, cfg.ReposRoot, storage.InitEmptyStore, logger)
+ icept := newInterceptor(cfg.Conf, serviceName, normalizeAud(cfg.HttpHost), cfg.DB, keys, cfg.ReposRoot, storage.InitEmptyStore)
// Load-bearing (see storage/init.go): the FS MUST be rooted at ReposRoot via
// LocalFilesysWithWorkingDir so sealed chunk-download URLs carry clean
@@ 105,7 108,7 @@ func New(cfg Config) (*Server, error) {
}
srv, err := remotesrv.NewServer(remotesrv.ServerArgs{
- Logger: logger,
+ Logger: cfg.DoltLogger,
HttpHost: cfg.HttpHost,
HttpListenAddr: cfg.ListenAddr,
GrpcListenAddr: cfg.ListenAddr, // == HttpListenAddr ⇒ single h2c port
@@ 117,7 120,7 @@ func New(cfg Config) (*Server, error) {
})
if err != nil {
if cerr := cache.Close(); cerr != nil {
- logger.Warnf("remoteapi: closing cache after NewServer failure: %v", cerr)
+ logger.Warn("closing the chunk-store cache after a NewServer failure", scribe.Err(cerr))
}
return nil, fmt.Errorf("remoteapi: remotesrv.NewServer: %w", err)
}
@@ 145,7 148,7 @@ func (s *Server) Serve() error {
func (s *Server) GracefulStop() {
s.srv.GracefulStop()
if err := s.cache.Close(); err != nil {
- s.logger.Warnf("remoteapi: closing cache on shutdown: %v", err)
+ s.logger.Warn("closing the chunk-store cache on shutdown", scribe.Err(err))
}
}
M web/templates.go => web/templates.go +5 -2
@@ 5,11 5,13 @@ import (
"fmt"
"html/template"
"io/fs"
- "log"
+ "log/slog"
"net/http"
"net/url"
"strings"
+ "go.bigb.es/auxilia/scribe"
+
"sourcecraft.dev/bigbes/sr-ht-ecore/pages"
)
@@ 140,6 142,7 @@ func humanizeSize(n uint64) string {
// do with it here but say so.
func (a *app) render(w http.ResponseWriter, status int, page string, data any) {
if err := a.pages.Render(w, status, page, data); err != nil {
- log.Printf("web: %v", err)
+ slog.Error("rendering a page failed",
+ "component", "web", "page", page, "status", status, scribe.Err(err))
}
}