~bigbes/sr-ht-dolt

95bedef83a9a6095cb34fa5c12570630b0513ca4 — Eugene Blikh 9 days ago e04928c
logging: take the log policy from ecore

setupLogging's level parser, its os.Stderr.Stat colour probe and its three-key
mask list were one of six copies. Defaults(conf, section) resolves all of it now
and the handler stays here, which is the split that package documents.

Three things follow from taking the instance's list instead of this service's
own: the mask set gains the config-file private keys and the migration DSN that
siblings had already learned to redact, NO_COLOR is honoured, and verbosity can
be set for one run with $LOG_LEVEL or -d. [dolt.sr.ht]log-level is unchanged.
2 files changed, 33 insertions(+), 30 deletions(-)

M cmd/doltsrht/logging.go
M config.example.ini
M cmd/doltsrht/logging.go => cmd/doltsrht/logging.go +29 -30
@@ 1,15 1,13 @@
package main

import (
	"log/slog"
	"os"
	"time"

	"github.com/vaughan0/go-ini"

	"go.bigb.es/auxilia/scribe"

	"sourcecraft.dev/bigbes/sr-ht-core/config"
	"sourcecraft.dev/bigbes/sr-ht-ecore/logging"
)

// setupLogging installs the process-wide slog handler.


@@ 23,38 21,39 @@ import (
// 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.
// The policy is sr-ht-ecore's and the handler is ours, which is the split that
// package documents: the level, the colour decision and — the part that is not
// presentation — the set of attribute keys that must never reach a log file are
// facts about this instance and are maintained once, while the handler that
// spends them stays here, because a service wanting a JSON handler for a log
// shipper should not have to link a tinting one to share a mask list.
//
// The verbosity is [dolt.sr.ht]log-level ("debug", "info", "warn", "error") as
// before, and now also $LOG_LEVEL and the -d every SourceHut daemon takes. -d is
// resolved here rather than left to server.New because server.New runs after
// config loading and validation: a daemon that becomes verbose only once it has
// started is silent for exactly the window an operator passes -d to watch. 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
	}
	opts := logging.Defaults(conf, serviceName)

	slog.SetDefault(slog.New(scribe.NewTintHandler(
	logging.Install(scribe.NewTintHandler(
		scribe.WithWriter(os.Stderr),
		scribe.WithLevel(level),
		scribe.WithSource(true),
		scribe.WithTimeFormat(time.DateTime),
		scribe.WithLevel(opts.Level),
		scribe.WithSource(opts.AddSource),
		scribe.WithTimeFormat(opts.TimeFormat),
		// 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)),
		scribe.WithNoColor(!opts.Color),
		// 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
		// happens to contain the word "token". Taking the instance's list rather
		// than this service's three adds the keys the siblings had learned to
		// redact and this one had not — the network and webhook private keys out
		// of config.ini, and the migration DSN, which carries a password.
		scribe.WithMaskKeys(opts.MaskKeys...),
		scribe.WithMask(opts.MaskPattern, opts.MaskReplacement),
	))
}

M config.example.ini => config.example.ini +4 -0
@@ 42,6 42,10 @@ 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.
;
; This is the persistent setting. For one run, $LOG_LEVEL overrides it and the
; -d flag overrides both — and both apply from the first line of startup, before
; the config below has been read.
log-level=info

; ---------------------------------------------------------------------------