App v0.2.26 and gateway 0.1.20
Client: seek controls, Bazarr subtitle download and cast panel in the player; MDBList ratings strip; episode and schedule detail pages; series pace estimate; what's new panel; install-permission onboarding step; synced per-profile preferences; Emby outage banner. Gateway: rebuilt admin console (one fragment per page), preference history and restore, merged Continue Watching, Emby health probe, subtitle selection and Bazarr download, structured request logging with per-request identity, and embedded build version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2675e6d82b
commit
4a4df7a73c
@@ -28,10 +28,36 @@ func ParseLevel(value string) slog.Level {
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a text logger suited to `docker compose logs`. Fields remain structured
|
||||
// key=value pairs, but each event is one compact line rather than a JSON object.
|
||||
// Format selects how a record is rendered. The wire shape of a log line is operational
|
||||
// configuration, not a code decision: a person tailing `docker compose logs` and a
|
||||
// collector shipping the same stream elsewhere want different things from it.
|
||||
type Format int
|
||||
|
||||
const (
|
||||
// FormatConsole is the default: one aligned, human-readable line per event.
|
||||
FormatConsole Format = iota
|
||||
// FormatLogfmt is slog's own `time=… level=… msg=…` text output.
|
||||
FormatLogfmt
|
||||
// FormatJSON is one object per line, for a log collector.
|
||||
FormatJSON
|
||||
)
|
||||
|
||||
// ParseFormat is forgiving for the same reason [ParseLevel] is — a typo in Docker
|
||||
// configuration must not stop the gateway.
|
||||
func ParseFormat(value string) Format {
|
||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||
case "JSON":
|
||||
return FormatJSON
|
||||
case "LOGFMT", "TEXT":
|
||||
return FormatLogfmt
|
||||
default:
|
||||
return FormatConsole
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a logger suited to `docker compose logs`: one compact line per event.
|
||||
func New(w io.Writer, level slog.Leveler) *slog.Logger {
|
||||
logger, _ := NewBuffered(w, level, 0)
|
||||
logger, _ := NewBuffered(w, level, 0, FormatConsole)
|
||||
return logger
|
||||
}
|
||||
|
||||
@@ -75,8 +101,11 @@ func ParseCapacity(value string, fallback int) int {
|
||||
return capacity
|
||||
}
|
||||
|
||||
// NewBuffered writes normal text logs and mirrors accepted records into a ring buffer.
|
||||
func NewBuffered(w io.Writer, level slog.Leveler, capacity int) (*slog.Logger, *Buffer) {
|
||||
// NewBuffered writes lines in the requested format and mirrors accepted records into a
|
||||
// ring buffer, which is what the admin page's live log reads.
|
||||
func NewBuffered(
|
||||
w io.Writer, level slog.Leveler, capacity int, format Format,
|
||||
) (*slog.Logger, *Buffer) {
|
||||
options := &slog.HandlerOptions{
|
||||
Level: level,
|
||||
ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr {
|
||||
@@ -86,12 +115,20 @@ func NewBuffered(w io.Writer, level slog.Leveler, capacity int) (*slog.Logger, *
|
||||
return attr
|
||||
},
|
||||
}
|
||||
buffer := &Buffer{capacity: capacity}
|
||||
text := slog.NewTextHandler(w, options)
|
||||
if capacity <= 0 {
|
||||
return slog.New(text), buffer
|
||||
var written slog.Handler
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
written = slog.NewJSONHandler(w, options)
|
||||
case FormatLogfmt:
|
||||
written = slog.NewTextHandler(w, options)
|
||||
default:
|
||||
written = newConsoleHandler(w, level)
|
||||
}
|
||||
return slog.New(&captureHandler{next: text, buffer: buffer, level: level}), buffer
|
||||
buffer := &Buffer{capacity: capacity}
|
||||
if capacity <= 0 {
|
||||
return slog.New(written), buffer
|
||||
}
|
||||
return slog.New(&captureHandler{next: written, buffer: buffer, level: level}), buffer
|
||||
}
|
||||
|
||||
type captureHandler struct {
|
||||
@@ -150,17 +187,7 @@ func addAttribute(target map[string]string, groups []string, attr slog.Attr) {
|
||||
}
|
||||
return
|
||||
}
|
||||
switch attr.Value.Kind() {
|
||||
case slog.KindDuration:
|
||||
target[key] = attr.Value.Duration().String()
|
||||
case slog.KindTime:
|
||||
target[key] = attr.Value.Time().UTC().Format(time.RFC3339Nano)
|
||||
default:
|
||||
target[key] = attr.Value.String()
|
||||
if attr.Value.Kind() == slog.KindInt64 {
|
||||
target[key] = strconv.FormatInt(attr.Value.Int64(), 10)
|
||||
}
|
||||
}
|
||||
target[key] = attributeValue(attr.Value)
|
||||
}
|
||||
|
||||
func (b *Buffer) append(event Event) {
|
||||
|
||||
Reference in New Issue
Block a user