mirror of
https://github.com/syncthing/syncthing.git
synced 2025-12-23 22:18:14 -05:00
This adds several options for configuring the log format of timestamps
and severity levels, making it more suitable for integration with log
systems like systemd.
--log-format-timestamp="2006-01-02 15:04:05"
Format for timestamp, set to empty to disable timestamps ($STLOGFORMATTIMESTAMP)
--[no-]log-format-level-string
Whether to include level string in log line ($STLOGFORMATLEVELSTRING)
--[no-]log-format-level-syslog
Whether to include level as syslog prefix in log line ($STLOGFORMATLEVELSYSLOG)
So, to get a timestamp suitable for systemd (syslog prefix, no level
string, no timestamp) we can pass `--log-format-timestamp=""
--no-log-format-level-string --log-format-level-syslog` or,
equivalently, set `STLOGFORMATTIMESTAMP="" STLOGFORMATLEVELSTRING=false
STLOGFORMATLEVELSYSLOG=true`.
Signed-off-by: Jakob Borg <jakob@kastelo.net>
201 lines
4.6 KiB
Go
201 lines
4.6 KiB
Go
// Copyright (C) 2025 The Syncthing Authors.
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
package slogutil
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"path"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type LineFormat struct {
|
|
TimestampFormat string
|
|
LevelString bool
|
|
LevelSyslog bool
|
|
}
|
|
|
|
type formattingOptions struct {
|
|
LineFormat
|
|
|
|
out io.Writer
|
|
recs []*lineRecorder
|
|
timeOverride time.Time
|
|
}
|
|
|
|
type formattingHandler struct {
|
|
attrs []slog.Attr
|
|
groups []string
|
|
opts *formattingOptions
|
|
}
|
|
|
|
func SetLineFormat(f LineFormat) {
|
|
globalFormatter.LineFormat = f
|
|
}
|
|
|
|
var _ slog.Handler = (*formattingHandler)(nil)
|
|
|
|
func (h *formattingHandler) Enabled(context.Context, slog.Level) bool {
|
|
return true
|
|
}
|
|
|
|
func (h *formattingHandler) Handle(_ context.Context, rec slog.Record) error {
|
|
fr := runtime.CallersFrames([]uintptr{rec.PC})
|
|
var logAttrs []any
|
|
if fram, _ := fr.Next(); fram.Function != "" {
|
|
pkgName, typeName := funcNameToPkg(fram.Function)
|
|
lvl := globalLevels.Get(pkgName)
|
|
if lvl > rec.Level {
|
|
// Logging not enabled at the record's level
|
|
return nil
|
|
}
|
|
logAttrs = append(logAttrs, slog.String("pkg", pkgName))
|
|
if lvl <= slog.LevelDebug {
|
|
// We are debugging, add additional source line data
|
|
if typeName != "" {
|
|
logAttrs = append(logAttrs, slog.String("type", typeName))
|
|
}
|
|
logAttrs = append(logAttrs, slog.Group("src", slog.String("file", path.Base(fram.File)), slog.Int("line", fram.Line)))
|
|
}
|
|
}
|
|
|
|
var prefix string
|
|
if len(h.groups) > 0 {
|
|
prefix = strings.Join(h.groups, ".") + "."
|
|
}
|
|
|
|
// Build the message string.
|
|
var sb strings.Builder
|
|
sb.WriteString(rec.Message)
|
|
|
|
// Collect all the attributes, adding the handler prefix.
|
|
attrs := make([]slog.Attr, 0, rec.NumAttrs()+len(h.attrs)+1)
|
|
rec.Attrs(func(attr slog.Attr) bool {
|
|
attr.Key = prefix + attr.Key
|
|
attrs = append(attrs, attr)
|
|
return true
|
|
})
|
|
attrs = append(attrs, h.attrs...)
|
|
attrs = append(attrs, slog.Group("log", logAttrs...))
|
|
|
|
// Expand and format attributes
|
|
var attrCount int
|
|
for _, attr := range attrs {
|
|
for _, attr := range expandAttrs("", attr) {
|
|
appendAttr(&sb, "", attr, &attrCount)
|
|
}
|
|
}
|
|
if attrCount > 0 {
|
|
sb.WriteRune(')')
|
|
}
|
|
|
|
line := Line{
|
|
When: cmp.Or(h.opts.timeOverride, rec.Time),
|
|
Message: sb.String(),
|
|
Level: rec.Level,
|
|
}
|
|
|
|
// If there is a recorder, record the line.
|
|
for _, rec := range h.opts.recs {
|
|
rec.record(line)
|
|
}
|
|
|
|
// If there's an output, print the line.
|
|
if h.opts.out != nil {
|
|
_, _ = line.WriteTo(h.opts.out, h.opts.LineFormat)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func expandAttrs(prefix string, a slog.Attr) []slog.Attr {
|
|
if prefix != "" {
|
|
a.Key = prefix + "." + a.Key
|
|
}
|
|
val := a.Value.Resolve()
|
|
if val.Kind() != slog.KindGroup {
|
|
return []slog.Attr{a}
|
|
}
|
|
var attrs []slog.Attr
|
|
for _, attr := range val.Group() {
|
|
attrs = append(attrs, expandAttrs(a.Key, attr)...)
|
|
}
|
|
return attrs
|
|
}
|
|
|
|
func appendAttr(sb *strings.Builder, prefix string, a slog.Attr, attrCount *int) {
|
|
const confusables = ` "()[]{},`
|
|
if a.Key == "" {
|
|
return
|
|
}
|
|
sb.WriteRune(' ')
|
|
if *attrCount == 0 {
|
|
sb.WriteRune('(')
|
|
}
|
|
sb.WriteString(prefix)
|
|
sb.WriteString(a.Key)
|
|
sb.WriteRune('=')
|
|
v := a.Value.Resolve().String()
|
|
if v == "" || strings.ContainsAny(v, confusables) {
|
|
v = strconv.Quote(v)
|
|
}
|
|
sb.WriteString(v)
|
|
*attrCount++
|
|
}
|
|
|
|
func (h *formattingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
if len(h.groups) > 0 {
|
|
prefix := strings.Join(h.groups, ".") + "."
|
|
for i := range attrs {
|
|
attrs[i].Key = prefix + attrs[i].Key
|
|
}
|
|
}
|
|
return &formattingHandler{
|
|
attrs: append(h.attrs, attrs...),
|
|
groups: h.groups,
|
|
opts: h.opts,
|
|
}
|
|
}
|
|
|
|
func (h *formattingHandler) WithGroup(name string) slog.Handler {
|
|
if name == "" {
|
|
return h
|
|
}
|
|
return &formattingHandler{
|
|
attrs: h.attrs,
|
|
groups: append([]string{name}, h.groups...),
|
|
opts: h.opts,
|
|
}
|
|
}
|
|
|
|
func funcNameToPkg(fn string) (string, string) {
|
|
fn = strings.ToLower(fn)
|
|
fn = strings.TrimPrefix(fn, "github.com/syncthing/syncthing/lib/")
|
|
fn = strings.TrimPrefix(fn, "github.com/syncthing/syncthing/internal/")
|
|
|
|
pkgTypFn := strings.Split(fn, ".") // [package, type, method] or [package, function]
|
|
if len(pkgTypFn) <= 2 {
|
|
return pkgTypFn[0], ""
|
|
}
|
|
|
|
pkg := pkgTypFn[0]
|
|
// Remove parenthesis and asterisk from the type name
|
|
typ := strings.TrimLeft(strings.TrimRight(pkgTypFn[1], ")"), "(*")
|
|
// Skip certain type names that add no value
|
|
typ = strings.TrimSuffix(typ, "service")
|
|
switch typ {
|
|
case pkg, "", "serveparams":
|
|
return pkg, ""
|
|
default:
|
|
return pkg, typ
|
|
}
|
|
}
|