0.2.63 update
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user