0.2.63 update

This commit is contained in:
ponzischeme89
2026-08-14 11:47:32 +12:00
parent 9a8ecbdceb
commit 06b6490ac9
41 changed files with 921 additions and 179 deletions
+13 -11
View File
@@ -59,7 +59,7 @@ func (h *consoleHandler) Handle(_ context.Context, record slog.Record) error {
var line strings.Builder
line.WriteString(record.Time.UTC().Format(time.DateTime))
line.WriteByte(' ')
line.WriteString(pad(record.Level.String(), levelWidth))
line.WriteString(pad(LevelName(record.Level), levelWidth))
line.WriteByte(' ')
line.WriteString(pad(record.Message, messageWidth))
for _, f := range fields {
@@ -103,7 +103,7 @@ func appendField(target []field, groups []string, attr slog.Attr) []field {
return target
}
key := strings.Join(append(append([]string{}, groups...), attr.Key), ".")
return append(target, field{key: key, value: attributeValue(attr.Value)})
return append(target, field{key: key, value: safeAttribute(key, attributeValue(attr.Value))})
}
// fieldRank puts the fields that identify *who and where* first, in the same order on
@@ -111,15 +111,17 @@ func appendField(target []field, groups []string, attr slog.Attr) []field {
// Anything unranked keeps the order the caller wrote it in, which is usually the order
// that reads best for that particular event.
var fieldRank = map[string]int{
"component": 1,
"user": 2,
"device": 3,
"client": 4,
"protocol": 5,
"method": 6,
"path": 7,
"status": 8,
"duration": 9,
"component": 1,
"user": 2,
"device": 3,
"client": 4,
"correlation": 5,
"play_session_id": 6,
"protocol": 7,
"method": 8,
"path": 9,
"status": 10,
"duration": 11,
// Constant per process, so it belongs at the end of the line rather than in front
// of the fields that differ between events.
"version": 900,
+50 -2
View File
@@ -7,6 +7,7 @@ import (
"encoding/json"
"io"
"log/slog"
"net/url"
"os"
"path/filepath"
"strconv"
@@ -16,11 +17,25 @@ import (
"time"
)
// LevelTrace is deliberately below DEBUG. It is disabled unless MEMBY_LOG_LEVEL=TRACE,
// so tight diagnostic loops can be instrumented without making ordinary DEBUG unusable.
const LevelTrace slog.Level = slog.LevelDebug - 4
// LevelName presents custom levels consistently in every configured output format.
func LevelName(level slog.Level) string {
if level == LevelTrace {
return "TRACE"
}
return level.String()
}
// ParseLevel returns a supported slog level, defaulting to INFO for empty or invalid
// values. Keeping this forgiving prevents a typo in Docker configuration from stopping
// the gateway.
func ParseLevel(value string) slog.Level {
switch strings.ToUpper(strings.TrimSpace(value)) {
case "TRACE":
return LevelTrace
case "DEBUG":
return slog.LevelDebug
case "WARN", "WARNING":
@@ -128,6 +143,11 @@ func NewBuffered(
if attr.Key == slog.TimeKey {
return slog.String(slog.TimeKey, attr.Value.Time().UTC().Format(time.RFC3339))
}
if attr.Key == slog.LevelKey {
if level, ok := attr.Value.Any().(slog.Level); ok {
return slog.String(slog.LevelKey, LevelName(level))
}
}
return attr
},
}
@@ -233,7 +253,7 @@ func (h *captureHandler) Handle(ctx context.Context, record slog.Record) error {
})
h.buffer.append(Event{
OccurredAt: record.Time.UTC(),
Level: record.Level.String(),
Level: LevelName(record.Level),
Message: record.Message,
Attributes: attributes,
})
@@ -266,7 +286,35 @@ func addAttribute(target map[string]string, groups []string, attr slog.Attr) {
}
return
}
target[key] = attributeValue(attr.Value)
target[key] = safeAttribute(key, attributeValue(attr.Value))
}
// safeAttribute is the final guard before a structured record reaches the browser-visible
// buffer and optional on-disk history. Call sites should never log credentials, but this
// makes one accidental token-bearing URL or header non-disclosive by construction.
func safeAttribute(key, value string) string {
lower := strings.ToLower(key)
if strings.Contains(lower, "password") || strings.Contains(lower, "token") ||
strings.Contains(lower, "secret") || strings.Contains(lower, "cookie") ||
strings.Contains(lower, "authorization") || strings.Contains(lower, "api_key") || strings.Contains(lower, "apikey") {
return "[redacted]"
}
if parsed, err := url.Parse(value); err == nil && parsed.RawQuery != "" {
query := parsed.Query()
changed := false
for key := range query {
lowerKey := strings.ToLower(key)
if strings.Contains(lowerKey, "token") || strings.Contains(lowerKey, "key") || strings.Contains(lowerKey, "auth") || lowerKey == "t" {
query.Set(key, "[redacted]")
changed = true
}
}
if changed {
parsed.RawQuery = query.Encode()
return parsed.String()
}
}
return value
}
func (b *Buffer) append(event Event) {
+24
View File
@@ -63,6 +63,29 @@ func TestConsoleQuotesOnlyAmbiguousValues(t *testing.T) {
}
}
func TestLoggingRedactsSensitiveAttributesAndURLs(t *testing.T) {
var output bytes.Buffer
logger, buffer := NewBuffered(&output, LevelTrace, 10, FormatConsole)
logger.Log(nil, LevelTrace, "diagnostic request",
"authorization", "Bearer private-token",
"url", "https://emby.example/stream?api_key=private-key&quality=720p",
)
line := output.String()
if strings.Contains(line, "private-token") || strings.Contains(line, "private-key") {
t.Fatalf("console leaked a secret: %s", line)
}
if !strings.Contains(line, "TRACE") {
t.Fatalf("console did not render TRACE: %s", line)
}
page := buffer.Events(0, 1)
if len(page.Events) != 1 || page.Events[0].Level != "TRACE" ||
page.Events[0].Attributes["authorization"] != "[redacted]" ||
strings.Contains(page.Events[0].Attributes["url"], "private-key") {
t.Fatalf("buffer leaked or mislabelled diagnostic event: %+v", page.Events)
}
}
func TestParseFormat(t *testing.T) {
tests := map[string]Format{
"": FormatConsole,
@@ -144,6 +167,7 @@ func TestPersistentBufferRestoresTheRetainedTail(t *testing.T) {
func TestParseLevel(t *testing.T) {
tests := map[string]slog.Level{
"": slog.LevelInfo,
"trace": LevelTrace,
"debug": slog.LevelDebug,
"WARNING": slog.LevelWarn,
"error": slog.LevelError,