Logging: structured slog messages throughout, and WithLogger options so callers choose the logger
Log messages are lower-case event names with their details as key/value
attributes ("error" for errors), in place of formatted strings.
Loggable package take a WithLogger option as part of their "New*" call,
defaulting to slog.Default(). Each tags its lines with its `component`.
Some debug loggers print directly rather than using slog when in
terminal logging mode.
This commit is contained in:
1 parent
0b5fde9563
commit
043e307663
49 files changed
+598
-248
No files matched your search
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
@@ -21,15 +20,18 @@ type Generator struct {
|
||||
cat *Catalog
|
||||
store *Store
|
||||
rnd *rand.Rand
|
||||
opts options
|
||||
}
|
||||
|
||||
// NewGenerator returns a generator drawing from cat and storing releases in
|
||||
// store. The same seed and store contents give the same releases.
|
||||
func NewGenerator(cat *Catalog, store *Store, seed int64) *Generator {
|
||||
// store. The same seed and store contents give the same releases. It logs to
|
||||
// slog.Default() unless given WithLogger.
|
||||
func NewGenerator(cat *Catalog, store *Store, seed int64, opts ...Option) *Generator {
|
||||
return &Generator{
|
||||
cat: cat,
|
||||
store: store,
|
||||
rnd: rand.New(rand.NewSource(seed)), //nolint:gosec // seeded for repeatable mock data
|
||||
opts: applyOptions(opts),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,9 +237,10 @@ func (g *Generator) CatchUp(
|
||||
|
||||
// Run publishes a new release after a random gap between min and max until ctx ends.
|
||||
func (g *Generator) Run(ctx context.Context, min, max time.Duration) {
|
||||
logger := g.opts.logger("generator")
|
||||
for {
|
||||
d := g.gap(min, max)
|
||||
log.Printf("generator: next release in %s", d.Round(time.Second))
|
||||
logger.Info("next release", "in", d.Round(time.Second))
|
||||
|
||||
timer := time.NewTimer(d)
|
||||
select {
|
||||
@@ -249,11 +252,11 @@ func (g *Generator) Run(ctx context.Context, min, max time.Duration) {
|
||||
|
||||
r := g.Make(time.Now())
|
||||
if err := g.store.Append(r); err != nil {
|
||||
log.Printf("generator: storing release: %v", err)
|
||||
logger.Error("storing release failed", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("generator: published %s (%s) %s", r.ID, r.Tag[0], r.OCID)
|
||||
logger.Info("published release", "release_id", r.ID, "tag", r.Tag[0], "ocid", r.OCID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@@ -27,17 +26,20 @@ type server struct {
|
||||
store *Store
|
||||
baseURL string
|
||||
loc *time.Location // zone for updatedFrom/updatedTo without an offset; nil means UTC
|
||||
opts options
|
||||
}
|
||||
|
||||
// NewHandler serves release packages from store. baseURL is the public base
|
||||
// URL used in each package's uri and links.next. loc is the time zone that
|
||||
// updatedFrom and updatedTo are read in when they carry no offset, as the
|
||||
// documented yyyy-MM-ddTHH:mm:ss format doesn't (Europe/London for FTS).
|
||||
// Stored release dates are UTC; comparisons are on absolute instants.
|
||||
// Stored release dates are UTC; comparisons are on absolute instants. It
|
||||
// logs to slog.Default() unless given WithLogger.
|
||||
func NewHandler(
|
||||
store *Store, baseURL string, loc *time.Location,
|
||||
store *Store, baseURL string, loc *time.Location, opts ...Option,
|
||||
) http.HandlerFunc {
|
||||
return (&server{store: store, baseURL: baseURL, loc: loc}).handle
|
||||
s := &server{store: store, baseURL: baseURL, loc: loc, opts: applyOptions(opts)}
|
||||
return s.handle
|
||||
}
|
||||
|
||||
// encodeCursor makes the opaque cursor for a next link that resumes at store
|
||||
@@ -134,7 +136,7 @@ func (s *server) handle(w http.ResponseWriter, req *http.Request) {
|
||||
enc.SetIndent("", " ")
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(pkg); err != nil {
|
||||
log.Printf("handler: writing response: %v", err)
|
||||
s.opts.logger("handler").Warn("writing response failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// Option configures a Store, Generator or handler.
|
||||
type Option func(*options)
|
||||
|
||||
// options holds the settings Options change.
|
||||
type options struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// WithLogger makes a Store, Generator or handler log to logger instead of
|
||||
// slog.Default().
|
||||
func WithLogger(logger *slog.Logger) Option {
|
||||
return func(opts *options) {
|
||||
opts.log = logger
|
||||
}
|
||||
}
|
||||
|
||||
// applyOptions returns the settings opts make.
|
||||
func applyOptions(opts []Option) options {
|
||||
var settings options
|
||||
for _, opt := range opts {
|
||||
opt(&settings)
|
||||
}
|
||||
return settings
|
||||
}
|
||||
|
||||
// logger returns the logger to use, tagged with component: the given one, or
|
||||
// slog.Default(), looked up now.
|
||||
func (opts options) logger(component string) *slog.Logger {
|
||||
log := opts.log
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
}
|
||||
return log.With("component", component)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -33,19 +33,27 @@ type Store struct {
|
||||
// removing and picking one at random are all constant time.
|
||||
open []string
|
||||
openPos map[string]int
|
||||
|
||||
opts options
|
||||
}
|
||||
|
||||
// OpenStore loads the releases stored at path, creating the file and its
|
||||
// directory if needed, and opens it for appending. Lines that can't be read
|
||||
// are skipped, releases are put back in date order if needed, and a record
|
||||
// left incomplete by a crash is ended so new ones start on a fresh line.
|
||||
func OpenStore(path string) (*Store, error) {
|
||||
s := &Store{latest: map[string]int{}, lastNum: map[int]int{}, openPos: map[string]int{}}
|
||||
// It logs to slog.Default() unless given WithLogger.
|
||||
func OpenStore(path string, opts ...Option) (*Store, error) {
|
||||
s := &Store{
|
||||
latest: map[string]int{},
|
||||
lastNum: map[int]int{},
|
||||
openPos: map[string]int{},
|
||||
opts: applyOptions(opts),
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
releases, err := readReleases(path)
|
||||
releases, err := readReleases(s.opts.logger("store"), path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -67,9 +75,10 @@ func OpenStore(path string) (*Store, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// readReleases reads the releases stored at path, skipping lines that are
|
||||
// blank or can't be decoded. A missing file holds no releases.
|
||||
func readReleases(path string) ([]ocds.Release, error) {
|
||||
// readReleases reads the releases stored at path, skipping blank lines and
|
||||
// lines that can't be decoded (which are logged to logger). A missing file
|
||||
// holds no releases.
|
||||
func readReleases(logger *slog.Logger, path string) ([]ocds.Release, error) {
|
||||
in, err := os.Open(filepath.Clean(path))
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
@@ -89,7 +98,7 @@ func readReleases(path string) ([]ocds.Release, error) {
|
||||
}
|
||||
var r ocds.Release
|
||||
if err := json.Unmarshal(b, &r); err != nil {
|
||||
log.Printf("store: skipping unreadable line %d: %v", line, err)
|
||||
logger.Warn("skipping unreadable line", "line", line, "error", err)
|
||||
continue
|
||||
}
|
||||
releases = append(releases, r)
|
||||
|
||||
Reference in new issue
Block a user