diff --git a/cmd/gotosocial/common.go b/cmd/gotosocial/common.go
index bfa5b656d..4aa468ebf 100644
--- a/cmd/gotosocial/common.go
+++ b/cmd/gotosocial/common.go
@@ -64,11 +64,17 @@ func preRun(a preRunArgs) error {
// context, after initializing any last-minute things like loggers etc.
func run(ctx context.Context, action action.GTSAction) error {
log.SetTimeFormat(config.GetLogTimestampFormat())
- // Set the global log level from configuration
+
+ // Set the global log level from configuration.
if err := log.ParseLevel(config.GetLogLevel()); err != nil {
return fmt.Errorf("error parsing log level: %w", err)
}
+ // Set global log output format from configuration.
+ if err := log.ParseFormat(config.GetLogFormat()); err != nil {
+ return fmt.Errorf("error parsing log format: %w", err)
+ }
+
if config.GetSyslogEnabled() {
// Enable logging to syslog
if err := log.EnableSyslog(
diff --git a/docs/configuration/general.md b/docs/configuration/general.md
index 16589c842..3443bed35 100644
--- a/docs/configuration/general.md
+++ b/docs/configuration/general.md
@@ -26,6 +26,19 @@ log-db-queries: false
# Default: true
log-client-ip: true
+# String. Format to use for formatting log entries.
+# Supports "logfmt" and "json", with examples below:
+#
+# logfmt:
+# func=router.(*Router).Start.func1 level=INFO msg="listening on 127.0.0.1:8080"
+#
+# json:
+# {"func":"router.(*Router).Start.func1", "level":"INFO", "msg":"listening on 127.0.0.1:8080"}
+#
+# Examples: ["logfmt", "json"]
+# Default: "logfmt"
+log-format: "logfmt"
+
# String. Format to use for the timestamp in log lines.
# If set to the empty string, the timestamp will be
# ommitted from the logs entirely.
diff --git a/example/config.yaml b/example/config.yaml
index 64b4f2de2..3c573e01a 100644
--- a/example/config.yaml
+++ b/example/config.yaml
@@ -36,6 +36,19 @@ log-db-queries: false
# Default: true
log-client-ip: true
+# String. Format to use for formatting log entries.
+# Supports "logfmt" and "json", with examples below:
+#
+# logfmt:
+# func=router.(*Router).Start.func1 level=INFO msg="listening on 127.0.0.1:8080"
+#
+# json:
+# {"func":"router.(*Router).Start.func1", "level":"INFO", "msg":"listening on 127.0.0.1:8080"}
+#
+# Examples: ["logfmt", "json"]
+# Default: "logfmt"
+log-format: "logfmt"
+
# String. Format to use for the timestamp in log lines.
# If set to the empty string, the timestamp will be
# ommitted from the logs entirely.
diff --git a/go.mod b/go.mod
index bd76e818a..4f1de1073 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,6 @@
module code.superseriousbusiness.org/gotosocial
-go 1.24
-
-toolchain go1.24.3
+go 1.24.5
// Replace go-swagger with our version that fixes (ours particularly) use of Go1.23
replace github.com/go-swagger/go-swagger => codeberg.org/superseriousbusiness/go-swagger v0.32.3-gts-go1.23-fix
@@ -19,12 +17,13 @@ require (
codeberg.org/gruf/go-bytesize v1.0.3
codeberg.org/gruf/go-byteutil v1.3.0
codeberg.org/gruf/go-cache/v3 v3.6.1
+ codeberg.org/gruf/go-caller v0.0.0-20250806133437-db8d0b1f71cf
codeberg.org/gruf/go-debug v1.3.0
codeberg.org/gruf/go-errors/v2 v2.3.2
codeberg.org/gruf/go-fastcopy v1.1.3
codeberg.org/gruf/go-ffmpreg v0.6.8
codeberg.org/gruf/go-iotools v0.0.0-20240710125620-934ae9c654cf
- codeberg.org/gruf/go-kv/v2 v2.0.3
+ codeberg.org/gruf/go-kv/v2 v2.0.5
codeberg.org/gruf/go-list v0.0.0-20240425093752-494db03d641f
codeberg.org/gruf/go-mempool v0.0.0-20240507125005-cef10d64a760
codeberg.org/gruf/go-mutexes v1.5.2
diff --git a/go.sum b/go.sum
index 2497430df..4dcafd416 100644
--- a/go.sum
+++ b/go.sum
@@ -18,6 +18,8 @@ codeberg.org/gruf/go-byteutil v1.3.0 h1:nRqJnCcRQ7xbfU6azw7zOzJrSMDIJHBqX6FL9vEM
codeberg.org/gruf/go-byteutil v1.3.0/go.mod h1:chgnZz1LUcfaObaIFglxF5MRYQkJGjQf4WwVz95ccCM=
codeberg.org/gruf/go-cache/v3 v3.6.1 h1:sY1XhYeskjZAuYeMm5R0o4Qymru5taNbzmZPSn1oXLE=
codeberg.org/gruf/go-cache/v3 v3.6.1/go.mod h1:JUNjc4E8gRccn3t+B99akxURFrU6NTDkvFVcwiZirnw=
+codeberg.org/gruf/go-caller v0.0.0-20250806133437-db8d0b1f71cf h1:Rzu7WLpscj2w1N+ClIHlJoTYf9SuqZrZ7E4f9T7jGdw=
+codeberg.org/gruf/go-caller v0.0.0-20250806133437-db8d0b1f71cf/go.mod h1:jEyYiqCzH1TaxfclSFYthE32oI0dsMnRS6EHqy6y0uo=
codeberg.org/gruf/go-debug v1.3.0 h1:PIRxQiWUFKtGOGZFdZ3Y0pqyfI0Xr87j224IYe2snZs=
codeberg.org/gruf/go-debug v1.3.0/go.mod h1:N+vSy9uJBQgpQcJUqjctvqFz7tBHJf+S/PIjLILzpLg=
codeberg.org/gruf/go-errors/v2 v2.3.2 h1:8ItWaOMfhDaqrJK1Pw8MO0Nu+o/tVcQtR5cJ58Vc4zo=
@@ -32,8 +34,8 @@ codeberg.org/gruf/go-iotools v0.0.0-20240710125620-934ae9c654cf h1:84s/ii8N6lYls
codeberg.org/gruf/go-iotools v0.0.0-20240710125620-934ae9c654cf/go.mod h1:zZAICsp5rY7+hxnws2V0ePrWxE0Z2Z/KXcN3p/RQCfk=
codeberg.org/gruf/go-kv v1.6.5 h1:ttPf0NA8F79pDqBttSudPTVCZmGncumeNIxmeM9ztz0=
codeberg.org/gruf/go-kv v1.6.5/go.mod h1:c4PsGqw05bDScvISpK+d31SiDEpBorweCL50hsiK3dc=
-codeberg.org/gruf/go-kv/v2 v2.0.3 h1:Ge4/WFR417EFPwfDdsf8S80XAdKF74RJk5g+VerAg1k=
-codeberg.org/gruf/go-kv/v2 v2.0.3/go.mod h1:mNL6SrBnYGEyrx6Mh4E1tAdhO0+T9/1iBrPJxIwxY24=
+codeberg.org/gruf/go-kv/v2 v2.0.5 h1:FuUAJcdWrj1jzySalGpe8sZSvBLr+LbvZiHMjt014s0=
+codeberg.org/gruf/go-kv/v2 v2.0.5/go.mod h1:mNL6SrBnYGEyrx6Mh4E1tAdhO0+T9/1iBrPJxIwxY24=
codeberg.org/gruf/go-list v0.0.0-20240425093752-494db03d641f h1:Ss6Z+vygy+jOGhj96d/GwsYYDd22QmIcH74zM7/nQkw=
codeberg.org/gruf/go-list v0.0.0-20240425093752-494db03d641f/go.mod h1:F9pl4h34iuVN7kucKam9fLwsItTc+9mmaKt7pNXRd/4=
codeberg.org/gruf/go-loosy v0.0.0-20231007123304-bb910d1ab5c4 h1:IXwfoU7f2whT6+JKIKskNl/hBlmWmnF1vZd84Eb3cyA=
diff --git a/internal/ap/extract.go b/internal/ap/extract.go
index 15d24cd1e..7a727b005 100644
--- a/internal/ap/extract.go
+++ b/internal/ap/extract.go
@@ -97,11 +97,12 @@ func ExtractActivityData(activity pub.Activity, rawJSON map[string]any) ([]TypeO
func ExtractAccountables(arr []TypeOrIRI) ([]Accountable, []TypeOrIRI) {
var accounts []Accountable
- for i := 0; i < len(arr); i++ {
+ for i := 0; i < len(arr); {
elem := arr[i]
if elem.IsIRI() {
// skip IRIs
+ i++ // iter
continue
}
@@ -112,6 +113,7 @@ func ExtractAccountables(arr []TypeOrIRI) ([]Accountable, []TypeOrIRI) {
// Try cast AS type as Accountable.
account, ok := ToAccountable(t)
if !ok {
+ i++ // iter
continue
}
@@ -130,11 +132,12 @@ func ExtractAccountables(arr []TypeOrIRI) ([]Accountable, []TypeOrIRI) {
func ExtractStatusables(arr []TypeOrIRI) ([]Statusable, []TypeOrIRI) {
var statuses []Statusable
- for i := 0; i < len(arr); i++ {
+ for i := 0; i < len(arr); {
elem := arr[i]
if elem.IsIRI() {
// skip IRIs
+ i++ // iter
continue
}
@@ -145,10 +148,11 @@ func ExtractStatusables(arr []TypeOrIRI) ([]Statusable, []TypeOrIRI) {
// Try cast AS type as Statusable.
status, ok := ToStatusable(t)
if !ok {
+ i++ // iter
continue
}
- // Add casted Statusable type.
+ // Append casted Statusable type.
statuses = append(statuses, status)
// Drop elem from slice.
@@ -163,11 +167,12 @@ func ExtractStatusables(arr []TypeOrIRI) ([]Statusable, []TypeOrIRI) {
func ExtractPollOptionables(arr []TypeOrIRI) ([]PollOptionable, []TypeOrIRI) {
var options []PollOptionable
- for i := 0; i < len(arr); i++ {
+ for i := 0; i < len(arr); {
elem := arr[i]
if elem.IsIRI() {
// skip IRIs
+ i++ // iter
continue
}
@@ -178,6 +183,7 @@ func ExtractPollOptionables(arr []TypeOrIRI) ([]PollOptionable, []TypeOrIRI) {
// Try cast as PollOptionable.
option, ok := ToPollOptionable(t)
if !ok {
+ i++ // iter
continue
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 95d74342e..f7a99c35f 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -55,6 +55,7 @@ func fieldtag(field, tag string) string {
// https://github.com/mvdan/gofumpt.
type Configuration struct {
LogLevel string `name:"log-level" usage:"Log level to run at: [trace, debug, info, warn, fatal]"`
+ LogFormat string `name:"log-format" usage:"Log output format: [logfmt, json]"`
LogTimestampFormat string `name:"log-timestamp-format" usage:"Format to use for the log timestamp, as supported by Go's time.Layout"`
LogDbQueries bool `name:"log-db-queries" usage:"Log database queries verbosely when log-level is trace or debug"`
LogClientIP bool `name:"log-client-ip" usage:"Include the client IP in logs"`
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index 1540cc76b..df3b64b40 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -29,6 +29,7 @@ import (
// if you use this, you will still need to set Host, and, if desired, ConfigPath.
var Defaults = Configuration{
LogLevel: "info",
+ LogFormat: "logfmt",
LogTimestampFormat: "02/01/2006 15:04:05.000",
LogDbQueries: false,
ApplicationName: "gotosocial",
diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go
index 4aea742c1..a5cbf4c46 100644
--- a/internal/config/helpers.gen.go
+++ b/internal/config/helpers.gen.go
@@ -30,6 +30,7 @@ import (
const (
LogLevelFlag = "log-level"
+ LogFormatFlag = "log-format"
LogTimestampFormatFlag = "log-timestamp-format"
LogDbQueriesFlag = "log-db-queries"
LogClientIPFlag = "log-client-ip"
@@ -226,6 +227,7 @@ const (
func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {
flags.String("log-level", cfg.LogLevel, "Log level to run at: [trace, debug, info, warn, fatal]")
+ flags.String("log-format", cfg.LogFormat, "Log output format: [logfmt, json]")
flags.String("log-timestamp-format", cfg.LogTimestampFormat, "Format to use for the log timestamp, as supported by Go's time.Layout")
flags.Bool("log-db-queries", cfg.LogDbQueries, "Log database queries verbosely when log-level is trace or debug")
flags.Bool("log-client-ip", cfg.LogClientIP, "Include the client IP in logs")
@@ -412,8 +414,9 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {
}
func (cfg *Configuration) MarshalMap() map[string]any {
- cfgmap := make(map[string]any, 193)
+ cfgmap := make(map[string]any, 194)
cfgmap["log-level"] = cfg.LogLevel
+ cfgmap["log-format"] = cfg.LogFormat
cfgmap["log-timestamp-format"] = cfg.LogTimestampFormat
cfgmap["log-db-queries"] = cfg.LogDbQueries
cfgmap["log-client-ip"] = cfg.LogClientIP
@@ -623,6 +626,14 @@ func (cfg *Configuration) UnmarshalMap(cfgmap map[string]any) error {
}
}
+ if ival, ok := cfgmap["log-format"]; ok {
+ var err error
+ cfg.LogFormat, err = cast.ToStringE(ival)
+ if err != nil {
+ return fmt.Errorf("error casting %#v -> string for 'log-format': %w", ival, err)
+ }
+ }
+
if ival, ok := cfgmap["log-timestamp-format"]; ok {
var err error
cfg.LogTimestampFormat, err = cast.ToStringE(ival)
@@ -2218,6 +2229,28 @@ func GetLogLevel() string { return global.GetLogLevel() }
// SetLogLevel safely sets the value for global configuration 'LogLevel' field
func SetLogLevel(v string) { global.SetLogLevel(v) }
+// GetLogFormat safely fetches the Configuration value for state's 'LogFormat' field
+func (st *ConfigState) GetLogFormat() (v string) {
+ st.mutex.RLock()
+ v = st.config.LogFormat
+ st.mutex.RUnlock()
+ return
+}
+
+// SetLogFormat safely sets the Configuration value for state's 'LogFormat' field
+func (st *ConfigState) SetLogFormat(v string) {
+ st.mutex.Lock()
+ defer st.mutex.Unlock()
+ st.config.LogFormat = v
+ st.reloadToViper()
+}
+
+// GetLogFormat safely fetches the value for global configuration 'LogFormat' field
+func GetLogFormat() string { return global.GetLogFormat() }
+
+// SetLogFormat safely sets the value for global configuration 'LogFormat' field
+func SetLogFormat(v string) { global.SetLogFormat(v) }
+
// GetLogTimestampFormat safely fetches the Configuration value for state's 'LogTimestampFormat' field
func (st *ConfigState) GetLogTimestampFormat() (v string) {
st.mutex.RLock()
diff --git a/internal/db/bundb/hook.go b/internal/db/bundb/hook.go
index f02a1353d..cc71f655b 100644
--- a/internal/db/bundb/hook.go
+++ b/internal/db/bundb/hook.go
@@ -35,21 +35,24 @@ func (queryHook) BeforeQuery(ctx context.Context, _ *bun.QueryEvent) context.Con
// AfterQuery logs the time taken to query, the operation (select, update, etc), and the query itself as translated by bun.
func (queryHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) {
- // Get the DB query duration
+ // Get the database query duration.
dur := time.Since(event.StartTime)
switch {
- // Warn on slow database queries
+ // Warn on slow queries.
case dur > time.Second:
log.WithContext(ctx).
WithFields(kv.Fields{
{"duration", dur},
{"query", event.Query},
- }...).Warn("SLOW DATABASE QUERY")
+ }...).
+ Warn("SLOW DATABASE QUERY")
- // On trace, we log query information,
- // manually crafting so DB query not escaped.
+ // On trace log query info.
case log.Level() >= log.TRACE:
- log.Printf("level=TRACE duration=%s query=%s", dur, event.Query)
+ log.TraceKVs(ctx, kv.Fields{
+ {K: "duration", V: dur},
+ {K: "query", V: event.Query},
+ }...)
}
}
diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go
index f235e729e..8124c8dc5 100644
--- a/internal/federation/federatingdb/util.go
+++ b/internal/federation/federatingdb/util.go
@@ -20,6 +20,7 @@ package federatingdb
import (
"context"
"encoding/json"
+ "fmt"
"net/url"
"code.superseriousbusiness.org/activity/streams"
@@ -212,15 +213,23 @@ func getActivityContext(ctx context.Context) activityContext {
// lazy-serialization along with error output.
type serialize struct{ item vocab.Type }
+func (s serialize) MarshalJSON() ([]byte, error) {
+ m, err := ap.Serialize(s.item)
+ if err != nil {
+ return nil, fmt.Errorf("error serializing: %w", err)
+ }
+ return json.Marshal(m)
+}
+
func (s serialize) String() string {
m, err := ap.Serialize(s.item)
if err != nil {
- return "!(error serializing item: " + err.Error() + ")"
+ return "!(error serializing: " + err.Error() + ")"
}
b, err := json.Marshal(m)
if err != nil {
- return "!(error json marshaling item: " + err.Error() + ")"
+ return "!(error marshaling: " + err.Error() + ")"
}
return byteutil.B2S(b)
diff --git a/internal/gtserror/new_caller.go b/internal/gtserror/new_caller.go
index f7e0c84f6..7f4cd31cd 100644
--- a/internal/gtserror/new_caller.go
+++ b/internal/gtserror/new_caller.go
@@ -24,6 +24,8 @@ import (
"fmt"
"runtime"
"strings"
+
+ "codeberg.org/gruf/go-caller"
)
// Caller returns whether created errors will prepend calling function name.
@@ -48,7 +50,7 @@ func (ce *cerror) Unwrap() error {
// newAt is the same as New() but allows specifying calldepth.
func newAt(calldepth int, msg string) error {
return &cerror{
- c: caller(calldepth + 1),
+ c: getCaller(calldepth + 1),
e: errors.New(msg),
}
}
@@ -56,37 +58,25 @@ func newAt(calldepth int, msg string) error {
// newfAt is the same as Newf() but allows specifying calldepth.
func newfAt(calldepth int, msgf string, args ...any) error {
return &cerror{
- c: caller(calldepth + 1),
+ c: getCaller(calldepth + 1),
e: fmt.Errorf(msgf, args...),
}
}
-// caller fetches the calling function name, skipping 'depth'.
-func caller(depth int) string {
- var pcs [1]uintptr
+// getCaller fetches the calling function name, skipping 'depth'.
+func getCaller(depth int) string {
+ pcs := make([]uintptr, 1)
- // Fetch calling function using calldepth
- _ = runtime.Callers(depth, pcs[:])
- fn := runtime.FuncForPC(pcs[0])
+ // Fetch calling function at depth.
+ _ = runtime.Callers(depth, pcs)
- if fn == nil {
- return ""
- }
+ // Get cached calling func name.
+ name := caller.Get(pcs[0])
- // Get func name.
- name := fn.Name()
-
- // Drop everything but but function name itself
+ // Drop package / everything but function name itself.
if idx := strings.LastIndexByte(name, '.'); idx >= 0 {
name = name[idx+1:]
}
- const params = `[...]`
-
- // Drop any generic type parameter markers
- if idx := strings.Index(name, params); idx >= 0 {
- name = name[:idx] + name[idx+len(params):]
- }
-
return name
}
diff --git a/internal/log/caller.go b/internal/log/caller.go
index 5385b63b5..a67502b41 100644
--- a/internal/log/caller.go
+++ b/internal/log/caller.go
@@ -19,7 +19,8 @@ package log
import (
"runtime"
- "strings"
+
+ "codeberg.org/gruf/go-caller"
)
// Caller fetches the calling function name, skipping 'depth'.
@@ -27,29 +28,6 @@ import (
//go:noinline
func Caller(depth int) string {
pcs := make([]uintptr, 1)
-
- // Fetch calling func using depth.
_ = runtime.Callers(depth, pcs)
- fn := runtime.FuncForPC(pcs[0])
-
- if fn == nil {
- return ""
- }
-
- // Get func name.
- name := fn.Name()
-
- // Drop all but package and function name, no path.
- if idx := strings.LastIndex(name, "/"); idx >= 0 {
- name = name[idx+1:]
- }
-
- const params = `[...]`
-
- // Drop any function generic type parameter markers.
- if idx := strings.Index(name, params); idx >= 0 {
- name = name[:idx] + name[idx+len(params):]
- }
-
- return name
+ return caller.Get(pcs[0])
}
diff --git a/internal/log/entry.go b/internal/log/entry.go
index 48059c3bf..5b18ddf0f 100644
--- a/internal/log/entry.go
+++ b/internal/log/entry.go
@@ -48,117 +48,85 @@ func (e Entry) WithFields(kvs ...kv.Field) Entry {
}
// Trace will log formatted args as 'msg' field to the log at TRACE level.
-//
-//go:noinline
func (e Entry) Trace(a ...interface{}) {
- logf(e.ctx, 3, TRACE, e.kvs, args(len(a)), a...)
+ logf(e.ctx, TRACE, e.kvs, "", a...)
}
// Tracef will log format string as 'msg' field to the log at TRACE level.
-//
-//go:noinline
func (e Entry) Tracef(s string, a ...interface{}) {
- logf(e.ctx, 3, TRACE, e.kvs, s, a...)
+ logf(e.ctx, TRACE, e.kvs, s, a...)
}
// Debug will log formatted args as 'msg' field to the log at DEBUG level.
-//
-//go:noinline
func (e Entry) Debug(a ...interface{}) {
- logf(e.ctx, 3, DEBUG, e.kvs, args(len(a)), a...)
+ logf(e.ctx, DEBUG, e.kvs, "", a...)
}
// Debugf will log format string as 'msg' field to the log at DEBUG level.
-//
-//go:noinline
func (e Entry) Debugf(s string, a ...interface{}) {
- logf(e.ctx, 3, DEBUG, e.kvs, s, a...)
+ logf(e.ctx, DEBUG, e.kvs, s, a...)
}
// Info will log formatted args as 'msg' field to the log at INFO level.
-//
-//go:noinline
func (e Entry) Info(a ...interface{}) {
- logf(e.ctx, 3, INFO, e.kvs, args(len(a)), a...)
+ logf(e.ctx, INFO, e.kvs, "", a...)
}
// Infof will log format string as 'msg' field to the log at INFO level.
-//
-//go:noinline
func (e Entry) Infof(s string, a ...interface{}) {
- logf(e.ctx, 3, INFO, e.kvs, s, a...)
+ logf(e.ctx, INFO, e.kvs, s, a...)
}
// Warn will log formatted args as 'msg' field to the log at WARN level.
-//
-//go:noinline
func (e Entry) Warn(a ...interface{}) {
- logf(e.ctx, 3, WARN, e.kvs, args(len(a)), a...)
+ logf(e.ctx, WARN, e.kvs, "", a...)
}
// Warnf will log format string as 'msg' field to the log at WARN level.
-//
-//go:noinline
func (e Entry) Warnf(s string, a ...interface{}) {
- logf(e.ctx, 3, WARN, e.kvs, s, a...)
+ logf(e.ctx, WARN, e.kvs, s, a...)
}
// Error will log formatted args as 'msg' field to the log at ERROR level.
-//
-//go:noinline
func (e Entry) Error(a ...interface{}) {
- logf(e.ctx, 3, ERROR, e.kvs, args(len(a)), a...)
+ logf(e.ctx, ERROR, e.kvs, "", a...)
}
// Errorf will log format string as 'msg' field to the log at ERROR level.
-//
-//go:noinline
func (e Entry) Errorf(s string, a ...interface{}) {
- logf(e.ctx, 3, ERROR, e.kvs, s, a...)
+ logf(e.ctx, ERROR, e.kvs, s, a...)
}
// Panic will log formatted args as 'msg' field to the log at PANIC level.
// This will then call panic causing the application to crash.
-//
-//go:noinline
func (e Entry) Panic(a ...interface{}) {
defer panic(fmt.Sprint(a...))
- logf(e.ctx, 3, PANIC, e.kvs, args(len(a)), a...)
+ logf(e.ctx, PANIC, e.kvs, "", a...)
}
// Panicf will log format string as 'msg' field to the log at PANIC level.
// This will then call panic causing the application to crash.
-//
-//go:noinline
func (e Entry) Panicf(s string, a ...interface{}) {
defer panic(fmt.Sprintf(s, a...))
- logf(e.ctx, 3, PANIC, e.kvs, s, a...)
+ logf(e.ctx, PANIC, e.kvs, s, a...)
}
// Log will log formatted args as 'msg' field to the log at given level.
-//
-//go:noinline
func (e Entry) Log(lvl LEVEL, a ...interface{}) {
- logf(e.ctx, 3, lvl, e.kvs, args(len(a)), a...)
+ logf(e.ctx, lvl, e.kvs, "", a...)
}
// Logf will log format string as 'msg' field to the log at given level.
-//
-//go:noinline
func (e Entry) Logf(lvl LEVEL, s string, a ...interface{}) {
- logf(e.ctx, 3, lvl, e.kvs, s, a...)
+ logf(e.ctx, lvl, e.kvs, s, a...)
}
// Print will log formatted args to the stdout log output.
-//
-//go:noinline
func (e Entry) Print(a ...interface{}) {
- printf(3, e.kvs, args(len(a)), a...)
+ logf(e.ctx, UNSET, e.kvs, "", a...)
}
// Printf will log format string to the stdout log output.
-//
-//go:noinline
func (e Entry) Printf(s string, a ...interface{}) {
- printf(3, e.kvs, s, a...)
+ logf(e.ctx, UNSET, e.kvs, s, a...)
}
diff --git a/internal/log/format/format.go b/internal/log/format/format.go
new file mode 100644
index 000000000..b7c650906
--- /dev/null
+++ b/internal/log/format/format.go
@@ -0,0 +1,90 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package format
+
+import (
+ "sync/atomic"
+ "time"
+
+ "code.superseriousbusiness.org/gotosocial/internal/log/level"
+ "codeberg.org/gruf/go-byteutil"
+ "codeberg.org/gruf/go-kv/v2"
+)
+
+var (
+ // ensure func signature conformance.
+ _ FormatFunc = (*Logfmt)(nil).Format
+ _ FormatFunc = (*JSON)(nil).Format
+)
+
+// FormatFunc defines a function capable of formatting a log entry (args = 1+) to a given buffer (args = 0).
+type FormatFunc func(buf *byteutil.Buffer, stamp time.Time, pc uintptr, lvl level.LEVEL, kvs []kv.Field, msg string) //nolint:revive
+
+type Base struct {
+ // TimeFormat defines time.Format() layout to
+ // use when appending a timestamp to log entry.
+ TimeFormat string
+
+ // stampCache caches recently formatted stamps.
+ //
+ // see the following benchmark:
+ // goos: linux
+ // goarch: amd64
+ // pkg: code.superseriousbusiness.org/gotosocial/internal/log/format
+ // cpu: AMD Ryzen 7 7840U w/ Radeon 780M Graphics
+ // BenchmarkStampCache
+ // BenchmarkStampCache-16 272199975 4.447 ns/op 0 B/op 0 allocs/op
+ // BenchmarkNoStampCache
+ // BenchmarkNoStampCache-16 76041058 15.94 ns/op 0 B/op 0 allocs/op
+ stampCache atomic.Pointer[struct {
+ stamp time.Time
+ format string
+ }]
+}
+
+// AppendFormatStamp will append given timestamp according to TimeFormat,
+// caching recently formatted stamp strings to reduce number of Format() calls.
+func (b *Base) AppendFormatStamp(buf *byteutil.Buffer, stamp time.Time) {
+ const precision = time.Millisecond
+
+ // Load cached stamp value.
+ last := b.stampCache.Load()
+
+ // Round stamp to min precision.
+ stamp = stamp.Round(precision)
+
+ // If a cached entry exists use this string.
+ if last != nil && stamp.Equal(last.stamp) {
+ buf.B = append(buf.B, last.format...)
+ return
+ }
+
+ // Else format new and store ASAP,
+ // i.e. ignoring any CAS result.
+ format := stamp.Format(b.TimeFormat)
+ b.stampCache.CompareAndSwap(last, &struct {
+ stamp time.Time
+ format string
+ }{
+ stamp: stamp,
+ format: format,
+ })
+
+ // Finally, append new timestamp.
+ buf.B = append(buf.B, format...)
+}
diff --git a/internal/log/format/format_test.go b/internal/log/format/format_test.go
new file mode 100644
index 000000000..a8d229eb7
--- /dev/null
+++ b/internal/log/format/format_test.go
@@ -0,0 +1,60 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package format_test
+
+import (
+ "testing"
+ "time"
+
+ "code.superseriousbusiness.org/gotosocial/internal/log/format"
+ "codeberg.org/gruf/go-byteutil"
+)
+
+func BenchmarkStampCache(b *testing.B) {
+ var base format.Base
+ base.TimeFormat = `02/01/2006 15:04:05.000`
+
+ b.RunParallel(func(pb *testing.PB) {
+ var buf byteutil.Buffer
+ buf.B = make([]byte, 0, 1024)
+
+ for pb.Next() {
+ base.AppendFormatStamp(&buf, time.Now())
+ buf.B = buf.B[:0]
+ }
+
+ buf.B = buf.B[:0]
+ })
+}
+
+func BenchmarkNoStampCache(b *testing.B) {
+ var base format.Base
+ base.TimeFormat = `02/01/2006 15:04:05.000`
+
+ b.RunParallel(func(pb *testing.PB) {
+ var buf byteutil.Buffer
+ buf.B = make([]byte, 0, 1024)
+
+ for pb.Next() {
+ buf.B = time.Now().AppendFormat(buf.B, base.TimeFormat)
+ buf.B = buf.B[:0]
+ }
+
+ buf.B = buf.B[:0]
+ })
+}
diff --git a/internal/log/format/json.go b/internal/log/format/json.go
new file mode 100644
index 000000000..49964b5fe
--- /dev/null
+++ b/internal/log/format/json.go
@@ -0,0 +1,241 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package format
+
+import (
+ "encoding/json"
+ "time"
+ "unicode/utf8"
+
+ "code.superseriousbusiness.org/gotosocial/internal/log/level"
+ "codeberg.org/gruf/go-byteutil"
+ "codeberg.org/gruf/go-caller"
+ "codeberg.org/gruf/go-kv/v2"
+)
+
+type JSON struct{ Base }
+
+func (fmt *JSON) Format(buf *byteutil.Buffer, stamp time.Time, pc uintptr, lvl level.LEVEL, kvs []kv.Field, msg string) {
+ // Prepend opening JSON brace.
+ buf.B = append(buf.B, `{`...)
+
+ if fmt.TimeFormat != "" {
+ // Append JSON formatted timestamp string.
+ buf.B = append(buf.B, `"timestamp":"`...)
+ fmt.AppendFormatStamp(buf, stamp)
+ buf.B = append(buf.B, `", `...)
+ }
+
+ // Append JSON formatted caller func.
+ buf.B = append(buf.B, `"func":"`...)
+ buf.B = append(buf.B, caller.Get(pc)...)
+ buf.B = append(buf.B, `", `...)
+
+ if lvl != level.UNSET {
+ // Append JSON formatted level string.
+ buf.B = append(buf.B, `"level":"`...)
+ buf.B = append(buf.B, lvl.String()...)
+ buf.B = append(buf.B, `", `...)
+ }
+
+ // Append JSON formatted fields.
+ for _, field := range kvs {
+ appendStringJSON(buf, field.K)
+ buf.B = append(buf.B, `:`...)
+ b, _ := json.Marshal(field.V)
+ buf.B = append(buf.B, b...)
+ buf.B = append(buf.B, `, `...)
+ }
+
+ if msg != "" {
+ // Append JSON formatted msg string.
+ buf.B = append(buf.B, `"msg":`...)
+ appendStringJSON(buf, msg)
+ } else if string(buf.B[len(buf.B)-2:]) == ", " {
+ // Drop the trailing ", ".
+ buf.B = buf.B[:len(buf.B)-2]
+ }
+
+ // Append closing JSON brace.
+ buf.B = append(buf.B, `}`...)
+}
+
+// appendStringJSON is modified from the encoding/json.appendString()
+// function, copied in here such that we can use it for key appending.
+func appendStringJSON(buf *byteutil.Buffer, src string) {
+ const hex = "0123456789abcdef"
+ buf.B = append(buf.B, '"')
+ start := 0
+ for i := 0; i < len(src); {
+ if b := src[i]; b < utf8.RuneSelf {
+ if jsonSafeSet[b] {
+ i++
+ continue
+ }
+ buf.B = append(buf.B, src[start:i]...)
+ switch b {
+ case '\\', '"':
+ buf.B = append(buf.B, '\\', b)
+ case '\b':
+ buf.B = append(buf.B, '\\', 'b')
+ case '\f':
+ buf.B = append(buf.B, '\\', 'f')
+ case '\n':
+ buf.B = append(buf.B, '\\', 'n')
+ case '\r':
+ buf.B = append(buf.B, '\\', 'r')
+ case '\t':
+ buf.B = append(buf.B, '\\', 't')
+ default:
+ // This encodes bytes < 0x20 except for \b, \f, \n, \r and \t.
+ buf.B = append(buf.B, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF])
+ }
+ i++
+ start = i
+ continue
+ }
+ n := len(src) - i
+ if n > utf8.UTFMax {
+ n = utf8.UTFMax
+ }
+ c, size := utf8.DecodeRuneInString(src[i : i+n])
+ if c == utf8.RuneError && size == 1 {
+ buf.B = append(buf.B, src[start:i]...)
+ buf.B = append(buf.B, `\ufffd`...)
+ i += size
+ start = i
+ continue
+ }
+ // U+2028 is LINE SEPARATOR.
+ // U+2029 is PARAGRAPH SEPARATOR.
+ // They are both technically valid characters in JSON strings,
+ // but don't work in JSONP, which has to be evaluated as JavaScript,
+ // and can lead to security holes there. It is valid JSON to
+ // escape them, so we do so unconditionally.
+ // See https://en.wikipedia.org/wiki/JSON#Safety.
+ if c == '\u2028' || c == '\u2029' {
+ buf.B = append(buf.B, src[start:i]...)
+ buf.B = append(buf.B, '\\', 'u', '2', '0', '2', hex[c&0xF])
+ i += size
+ start = i
+ continue
+ }
+ i += size
+ }
+ buf.B = append(buf.B, src[start:]...)
+ buf.B = append(buf.B, '"')
+}
+
+var jsonSafeSet = [utf8.RuneSelf]bool{
+ ' ': true,
+ '!': true,
+ '"': false,
+ '#': true,
+ '$': true,
+ '%': true,
+ '&': true,
+ '\'': true,
+ '(': true,
+ ')': true,
+ '*': true,
+ '+': true,
+ ',': true,
+ '-': true,
+ '.': true,
+ '/': true,
+ '0': true,
+ '1': true,
+ '2': true,
+ '3': true,
+ '4': true,
+ '5': true,
+ '6': true,
+ '7': true,
+ '8': true,
+ '9': true,
+ ':': true,
+ ';': true,
+ '<': true,
+ '=': true,
+ '>': true,
+ '?': true,
+ '@': true,
+ 'A': true,
+ 'B': true,
+ 'C': true,
+ 'D': true,
+ 'E': true,
+ 'F': true,
+ 'G': true,
+ 'H': true,
+ 'I': true,
+ 'J': true,
+ 'K': true,
+ 'L': true,
+ 'M': true,
+ 'N': true,
+ 'O': true,
+ 'P': true,
+ 'Q': true,
+ 'R': true,
+ 'S': true,
+ 'T': true,
+ 'U': true,
+ 'V': true,
+ 'W': true,
+ 'X': true,
+ 'Y': true,
+ 'Z': true,
+ '[': true,
+ '\\': false,
+ ']': true,
+ '^': true,
+ '_': true,
+ '`': true,
+ 'a': true,
+ 'b': true,
+ 'c': true,
+ 'd': true,
+ 'e': true,
+ 'f': true,
+ 'g': true,
+ 'h': true,
+ 'i': true,
+ 'j': true,
+ 'k': true,
+ 'l': true,
+ 'm': true,
+ 'n': true,
+ 'o': true,
+ 'p': true,
+ 'q': true,
+ 'r': true,
+ 's': true,
+ 't': true,
+ 'u': true,
+ 'v': true,
+ 'w': true,
+ 'x': true,
+ 'y': true,
+ 'z': true,
+ '{': true,
+ '|': true,
+ '}': true,
+ '~': true,
+ '\u007f': true,
+}
diff --git a/internal/log/format/logfmt.go b/internal/log/format/logfmt.go
new file mode 100644
index 000000000..a774be8c9
--- /dev/null
+++ b/internal/log/format/logfmt.go
@@ -0,0 +1,67 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package format
+
+import (
+ "time"
+
+ "code.superseriousbusiness.org/gotosocial/internal/log/level"
+ "codeberg.org/gruf/go-byteutil"
+ "codeberg.org/gruf/go-caller"
+ "codeberg.org/gruf/go-kv/v2"
+ "codeberg.org/gruf/go-kv/v2/format"
+)
+
+var args = format.DefaultArgs()
+
+type Logfmt struct{ Base }
+
+func (fmt *Logfmt) Format(buf *byteutil.Buffer, stamp time.Time, pc uintptr, lvl level.LEVEL, kvs []kv.Field, msg string) {
+ if fmt.TimeFormat != "" {
+ // Append formatted timestamp string.
+ buf.B = append(buf.B, `timestamp="`...)
+ fmt.AppendFormatStamp(buf, stamp)
+ buf.B = append(buf.B, `" `...)
+ }
+
+ // Append formatted calling func.
+ buf.B = append(buf.B, `func=`...)
+ buf.B = append(buf.B, caller.Get(pc)...)
+ buf.B = append(buf.B, ' ')
+
+ if lvl != level.UNSET {
+ // Append formatted level string.
+ buf.B = append(buf.B, `level=`...)
+ buf.B = append(buf.B, lvl.String()...)
+ buf.B = append(buf.B, ' ')
+ }
+
+ // Append formatted fields.
+ for _, field := range kvs {
+ kv.AppendQuoteString(buf, field.K)
+ buf.B = append(buf.B, '=')
+ buf.B = format.Global.Append(buf.B, field.V, args)
+ buf.B = append(buf.B, ' ')
+ }
+
+ if msg != "" {
+ // Append formatted msg string.
+ buf.B = append(buf.B, `msg=`...)
+ kv.AppendQuoteString(buf, msg)
+ }
+}
diff --git a/internal/log/init.go b/internal/log/init.go
index 850ae8b5d..3ec3b777c 100644
--- a/internal/log/init.go
+++ b/internal/log/init.go
@@ -23,7 +23,8 @@ import (
"strings"
)
-// ParseLevel will parse the log level from given string and set to appropriate LEVEL.
+// ParseLevel will parse the log level from
+// given string and set to appropriate LEVEL.
func ParseLevel(str string) error {
switch strings.ToLower(str) {
case "trace":
@@ -44,16 +45,22 @@ func ParseLevel(str string) error {
return nil
}
-// EnableSyslog will enabling logging to the syslog at given address.
-func EnableSyslog(proto, addr string) error {
- // Dial a connection to the syslog daemon
- writer, err := syslog.Dial(proto, addr, 0, "gotosocial")
- if err != nil {
- return err
+// ParseFormat will parse the log format from
+// given string and set appropriate formatter.
+func ParseFormat(str string) error {
+ switch strings.ToLower(str) {
+ case "json":
+ SetJSON(true)
+ case "", "logfmt":
+ SetJSON(false)
+ default:
+ return fmt.Errorf("unknown log format: %q", str)
}
-
- // Set the syslog writer
- sysout = writer
-
return nil
}
+
+// EnableSyslog will enabling logging to the syslog at given address.
+func EnableSyslog(proto, addr string) (err error) {
+ sysout, err = syslog.Dial(proto, addr, 0, "gotosocial")
+ return err
+}
diff --git a/internal/log/level.go b/internal/log/level.go
index be45e2964..5776db999 100644
--- a/internal/log/level.go
+++ b/internal/log/level.go
@@ -17,22 +17,18 @@
package log
-// LEVEL defines a level of logging.
-type LEVEL uint8
-
-// Default levels of logging.
-const (
- UNSET LEVEL = 0
- PANIC LEVEL = 1
- ERROR LEVEL = 100
- WARN LEVEL = 150
- INFO LEVEL = 200
- DEBUG LEVEL = 250
- TRACE LEVEL = 254
- ALL LEVEL = ^LEVEL(0)
+import (
+ "code.superseriousbusiness.org/gotosocial/internal/log/level"
)
-// CanLog returns whether an incoming log of 'lvl' can be logged against receiving level.
-func (loglvl LEVEL) CanLog(lvl LEVEL) bool {
- return loglvl > lvl
-}
+type LEVEL = level.LEVEL
+
+const (
+ PANIC = level.PANIC
+ ERROR = level.ERROR
+ WARN = level.WARN
+ INFO = level.INFO
+ DEBUG = level.DEBUG
+ TRACE = level.TRACE
+ UNSET = level.UNSET
+)
diff --git a/internal/log/level/level.go b/internal/log/level/level.go
new file mode 100644
index 000000000..11f2abb6d
--- /dev/null
+++ b/internal/log/level/level.go
@@ -0,0 +1,45 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package level
+
+// LEVEL defines a level of logging.
+type LEVEL uint8
+
+// Logging levels.
+const (
+ PANIC LEVEL = 1
+ ERROR LEVEL = 100
+ WARN LEVEL = 150
+ INFO LEVEL = 200
+ DEBUG LEVEL = 250
+ TRACE LEVEL = 254
+ UNSET LEVEL = ^LEVEL(0)
+)
+
+func (lvl LEVEL) String() string {
+ return strings[lvl]
+}
+
+var strings = [int(UNSET) + 1]string{
+ TRACE: "TRACE",
+ DEBUG: "DEBUG",
+ INFO: "INFO",
+ WARN: "WARN",
+ ERROR: "ERROR",
+ PANIC: "PANIC",
+}
diff --git a/internal/log/log.go b/internal/log/log.go
index afb1bcdb8..50236db6e 100644
--- a/internal/log/log.go
+++ b/internal/log/log.go
@@ -22,35 +22,31 @@ import (
"fmt"
"log/syslog"
"os"
- "strings"
+ "runtime"
"time"
+ "code.superseriousbusiness.org/gotosocial/internal/log/format"
+ "code.superseriousbusiness.org/gotosocial/internal/log/level"
"code.superseriousbusiness.org/gotosocial/internal/util/xslices"
"codeberg.org/gruf/go-kv/v2"
)
var (
// loglvl is the currently
- // set logging output
- loglvl LEVEL
+ // set logging output.
+ loglvl = level.UNSET
- // lvlstrs is the lookup table
- // of all log levels to strings.
- lvlstrs = [int(ALL) + 1]string{
- TRACE: "TRACE",
- DEBUG: "DEBUG",
- INFO: "INFO",
- WARN: "WARN",
- ERROR: "ERROR",
- PANIC: "PANIC",
- }
+ // appendFormat stores log
+ // entry formatting function.
+ appendFormat = (&format.Logfmt{
+ Base: format.Base{TimeFormat: timefmt},
+ }).Format
// syslog output, only set if enabled.
sysout *syslog.Writer
- // timefmt is the logging time format used, which includes
- // the full field and required quoting
- timefmt = `timestamp="02/01/2006 15:04:05.000" `
+ // timefmt is the logging time format used.
+ timefmt = `02/01/2006 15:04:05.000`
// ctxhooks allows modifying log content based on context.
ctxhooks []func(context.Context, []kv.Field) []kv.Field
@@ -61,12 +57,12 @@ func Hook(hook func(ctx context.Context, kvs []kv.Field) []kv.Field) {
ctxhooks = append(ctxhooks, hook)
}
-// Level returns the currently set log
+// Level returns the currently set log.
func Level() LEVEL {
return loglvl
}
-// SetLevel sets the max logging
+// SetLevel sets the max logging.
func SetLevel(lvl LEVEL) {
loglvl = lvl
}
@@ -78,11 +74,20 @@ func TimeFormat() string {
// SetTimeFormat sets the timestamp format to the given string.
func SetTimeFormat(format string) {
- if format == "" {
- timefmt = format
- return
+ timefmt = format
+}
+
+// SetJSON enables / disables JSON log output formatting.
+func SetJSON(enabled bool) {
+ if enabled {
+ var fmt format.JSON
+ fmt.TimeFormat = timefmt
+ appendFormat = fmt.Format
+ } else {
+ var fmt format.Logfmt
+ fmt.TimeFormat = timefmt
+ appendFormat = fmt.Format
}
- timefmt = `timestamp="` + format + `" `
}
// New starts a new log entry.
@@ -105,282 +110,166 @@ func WithFields(fields ...kv.Field) Entry {
return Entry{kvs: fields}
}
-// Note that most of the below logging
-// functions we specifically do NOT allow
-// the Go buildchain to inline, to ensure
-// expected behaviour in caller fetching.
-
// Trace will log formatted args as 'msg' field to the log at TRACE level.
-//
-//go:noinline
func Trace(ctx context.Context, a ...interface{}) {
- logf(ctx, 3, TRACE, nil, args(len(a)), a...)
+ logf(ctx, TRACE, nil, "", a...)
}
// Tracef will log format string as 'msg' field to the log at TRACE level.
-//
-//go:noinline
func Tracef(ctx context.Context, s string, a ...interface{}) {
- logf(ctx, 3, TRACE, nil, s, a...)
+ logf(ctx, TRACE, nil, s, a...)
}
// TraceKV will log the one key-value field to the log at TRACE level.
-//
-//go:noinline
func TraceKV(ctx context.Context, key string, value interface{}) {
- logf(ctx, 3, TRACE, []kv.Field{{K: key, V: value}}, "")
+ logf(ctx, TRACE, []kv.Field{{K: key, V: value}}, "")
}
// TraceKVs will log key-value fields to the log at TRACE level.
-//
-//go:noinline
func TraceKVs(ctx context.Context, kvs ...kv.Field) {
- logf(ctx, 3, TRACE, kvs, "")
+ logf(ctx, TRACE, kvs, "")
}
// Debug will log formatted args as 'msg' field to the log at DEBUG level.
-//
-//go:noinline
func Debug(ctx context.Context, a ...interface{}) {
- logf(ctx, 3, DEBUG, nil, args(len(a)), a...)
+ logf(ctx, DEBUG, nil, "", a...)
}
// Debugf will log format string as 'msg' field to the log at DEBUG level.
-//
-//go:noinline
func Debugf(ctx context.Context, s string, a ...interface{}) {
- logf(ctx, 3, DEBUG, nil, s, a...)
+ logf(ctx, DEBUG, nil, s, a...)
}
// DebugKV will log the one key-value field to the log at DEBUG level.
-//
-//go:noinline
func DebugKV(ctx context.Context, key string, value interface{}) {
- logf(ctx, 3, DEBUG, []kv.Field{{K: key, V: value}}, "")
+ logf(ctx, DEBUG, []kv.Field{{K: key, V: value}}, "")
}
// DebugKVs will log key-value fields to the log at DEBUG level.
-//
-//go:noinline
func DebugKVs(ctx context.Context, kvs ...kv.Field) {
- logf(ctx, 3, DEBUG, kvs, "")
+ logf(ctx, DEBUG, kvs, "")
}
// Info will log formatted args as 'msg' field to the log at INFO level.
-//
-//go:noinline
func Info(ctx context.Context, a ...interface{}) {
- logf(ctx, 3, INFO, nil, args(len(a)), a...)
+ logf(ctx, INFO, nil, "", a...)
}
// Infof will log format string as 'msg' field to the log at INFO level.
-//
-//go:noinline
func Infof(ctx context.Context, s string, a ...interface{}) {
- logf(ctx, 3, INFO, nil, s, a...)
+ logf(ctx, INFO, nil, s, a...)
}
// InfoKV will log the one key-value field to the log at INFO level.
-//
-//go:noinline
func InfoKV(ctx context.Context, key string, value interface{}) {
- logf(ctx, 3, INFO, []kv.Field{{K: key, V: value}}, "")
+ logf(ctx, INFO, []kv.Field{{K: key, V: value}}, "")
}
// InfoKVs will log key-value fields to the log at INFO level.
-//
-//go:noinline
func InfoKVs(ctx context.Context, kvs ...kv.Field) {
- logf(ctx, 3, INFO, kvs, "")
+ logf(ctx, INFO, kvs, "")
}
// Warn will log formatted args as 'msg' field to the log at WARN level.
-//
-//go:noinline
func Warn(ctx context.Context, a ...interface{}) {
- logf(ctx, 3, WARN, nil, args(len(a)), a...)
+ logf(ctx, WARN, nil, "", a...)
}
// Warnf will log format string as 'msg' field to the log at WARN level.
-//
-//go:noinline
func Warnf(ctx context.Context, s string, a ...interface{}) {
- logf(ctx, 3, WARN, nil, s, a...)
+ logf(ctx, WARN, nil, s, a...)
}
// WarnKV will log the one key-value field to the log at WARN level.
-//
-//go:noinline
func WarnKV(ctx context.Context, key string, value interface{}) {
- logf(ctx, 3, WARN, []kv.Field{{K: key, V: value}}, "")
+ logf(ctx, WARN, []kv.Field{{K: key, V: value}}, "")
}
// WarnKVs will log key-value fields to the log at WARN level.
-//
-//go:noinline
func WarnKVs(ctx context.Context, kvs ...kv.Field) {
- logf(ctx, 3, WARN, kvs, "")
+ logf(ctx, WARN, kvs, "")
}
// Error will log formatted args as 'msg' field to the log at ERROR level.
-//
-//go:noinline
func Error(ctx context.Context, a ...interface{}) {
- logf(ctx, 3, ERROR, nil, args(len(a)), a...)
+ logf(ctx, ERROR, nil, "", a...)
}
// Errorf will log format string as 'msg' field to the log at ERROR level.
-//
-//go:noinline
func Errorf(ctx context.Context, s string, a ...interface{}) {
- logf(ctx, 3, ERROR, nil, s, a...)
+ logf(ctx, ERROR, nil, s, a...)
}
// ErrorKV will log the one key-value field to the log at ERROR level.
-//
-//go:noinline
func ErrorKV(ctx context.Context, key string, value interface{}) {
- logf(ctx, 3, ERROR, []kv.Field{{K: key, V: value}}, "")
+ logf(ctx, ERROR, []kv.Field{{K: key, V: value}}, "")
}
// ErrorKVs will log key-value fields to the log at ERROR level.
-//
-//go:noinline
func ErrorKVs(ctx context.Context, kvs ...kv.Field) {
- logf(ctx, 3, ERROR, kvs, "")
+ logf(ctx, ERROR, kvs, "")
}
// Panic will log formatted args as 'msg' field to the log at PANIC level.
// This will then call panic causing the application to crash.
-//
-//go:noinline
func Panic(ctx context.Context, a ...interface{}) {
defer panic(fmt.Sprint(a...))
- logf(ctx, 3, PANIC, nil, args(len(a)), a...)
+ logf(ctx, PANIC, nil, "", a...)
}
// Panicf will log format string as 'msg' field to the log at PANIC level.
// This will then call panic causing the application to crash.
-//
-//go:noinline
func Panicf(ctx context.Context, s string, a ...interface{}) {
defer panic(fmt.Sprintf(s, a...))
- logf(ctx, 3, PANIC, nil, s, a...)
+ logf(ctx, PANIC, nil, s, a...)
}
// PanicKV will log the one key-value field to the log at PANIC level.
// This will then call panic causing the application to crash.
-//
-//go:noinline
func PanicKV(ctx context.Context, key string, value interface{}) {
defer panic(kv.Field{K: key, V: value}.String())
- logf(ctx, 3, PANIC, []kv.Field{{K: key, V: value}}, "")
+ logf(ctx, PANIC, []kv.Field{{K: key, V: value}}, "")
}
// PanicKVs will log key-value fields to the log at PANIC level.
// This will then call panic causing the application to crash.
-//
-//go:noinline
func PanicKVs(ctx context.Context, kvs ...kv.Field) {
defer panic(kv.Fields(kvs).String())
- logf(ctx, 3, PANIC, kvs, "")
+ logf(ctx, PANIC, kvs, "")
}
// Log will log formatted args as 'msg' field to the log at given level.
-//
-//go:noinline
-func Log(ctx context.Context, lvl LEVEL, a ...interface{}) {
- logf(ctx, 3, lvl, nil, args(len(a)), a...)
+func Log(ctx context.Context, lvl LEVEL, a ...interface{}) { //nolint:revive
+ logf(ctx, lvl, nil, "", a...)
}
// Logf will log format string as 'msg' field to the log at given level.
-//
-//go:noinline
-func Logf(ctx context.Context, lvl LEVEL, s string, a ...interface{}) {
- logf(ctx, 3, lvl, nil, s, a...)
+func Logf(ctx context.Context, lvl LEVEL, s string, a ...interface{}) { //nolint:revive
+ logf(ctx, lvl, nil, s, a...)
}
// LogKV will log the one key-value field to the log at given level.
-//
-//go:noinline
func LogKV(ctx context.Context, lvl LEVEL, key string, value interface{}) { //nolint:revive
- logf(ctx, 3, lvl, []kv.Field{{K: key, V: value}}, "")
+ logf(ctx, lvl, []kv.Field{{K: key, V: value}}, "")
}
// LogKVs will log key-value fields to the log at given level.
-//
-//go:noinline
func LogKVs(ctx context.Context, lvl LEVEL, kvs ...kv.Field) { //nolint:revive
- logf(ctx, 3, lvl, kvs, "")
+ logf(ctx, lvl, kvs, "")
}
// Print will log formatted args to the stdout log output.
-//
-//go:noinline
func Print(a ...interface{}) {
- printf(3, nil, args(len(a)), a...)
+ logf(context.Background(), UNSET, nil, "", a...)
}
// Printf will log format string to the stdout log output.
-//
-//go:noinline
func Printf(s string, a ...interface{}) {
- printf(3, nil, s, a...)
-}
-
-// PrintKVs will log the one key-value field to the stdout log output.
-//
-//go:noinline
-func PrintKV(key string, value interface{}) {
- printf(3, []kv.Field{{K: key, V: value}}, "")
-}
-
-// PrintKVs will log key-value fields to the stdout log output.
-//
-//go:noinline
-func PrintKVs(kvs ...kv.Field) {
- printf(3, kvs, "")
+ logf(context.Background(), UNSET, nil, s, a...)
}
//go:noinline
-func printf(depth int, fields []kv.Field, s string, a ...interface{}) {
- // Acquire buffer
- buf := getBuf()
-
- // Append formatted timestamp according to `timefmt`
- buf.B = time.Now().AppendFormat(buf.B, timefmt)
-
- // Append formatted caller func
- buf.B = append(buf.B, `func=`...)
- buf.B = append(buf.B, Caller(depth+1)...)
- buf.B = append(buf.B, ' ')
-
- if len(fields) > 0 {
- // Append formatted fields
- kv.Fields(fields).AppendFormat(buf, false)
- buf.B = append(buf.B, ' ')
- }
-
- // Append formatted args
- fmt.Fprintf(buf, s, a...)
-
- if buf.B[len(buf.B)-1] != '\n' {
- // Append a final newline
- buf.B = append(buf.B, '\n')
- }
-
- if sysout != nil {
- // Write log entry to syslog
- logsys(INFO, buf.String())
- }
-
- // Write to log and release
- _, _ = os.Stdout.Write(buf.B)
- putBuf(buf)
-}
-
-//go:noinline
-func logf(ctx context.Context, depth int, lvl LEVEL, fields []kv.Field, s string, a ...interface{}) {
+func logf(ctx context.Context, lvl LEVEL, fields []kv.Field, msg string, args ...interface{}) {
var out *os.File
// Check if enabled.
@@ -396,25 +285,20 @@ func logf(ctx context.Context, depth int, lvl LEVEL, fields []kv.Field, s string
out = os.Stdout
}
- // Acquire buffer
+ // Get log stamp.
+ now := time.Now()
+
+ // Get caller information.
+ pcs := make([]uintptr, 1)
+ _ = runtime.Callers(3, pcs)
+
+ // Acquire buffer.
buf := getBuf()
+ defer putBuf(buf)
- // Append formatted timestamp according to `timefmt`
- buf.B = time.Now().AppendFormat(buf.B, timefmt)
-
- // Append formatted caller func
- buf.B = append(buf.B, `func=`...)
- buf.B = append(buf.B, Caller(depth+1)...)
- buf.B = append(buf.B, ' ')
-
- // Append formatted level string
- buf.B = append(buf.B, `level=`...)
- buf.B = append(buf.B, lvlstrs[lvl]...)
- buf.B = append(buf.B, ' ')
-
- if ctx != nil && len(ctxhooks) > 0 {
- // Ensure fields have space for hooks (+1 for below).
- fields = xslices.GrowJust(fields, len(ctxhooks)+1)
+ if ctx != nil {
+ // Ensure fields have space for context hooks.
+ fields = xslices.GrowJust(fields, len(ctxhooks))
// Pass context through hooks.
for _, hook := range ctxhooks {
@@ -422,18 +306,32 @@ func logf(ctx context.Context, depth int, lvl LEVEL, fields []kv.Field, s string
}
}
- if s != "" {
- // Append message (if given) as final log field.
- fields = xslices.AppendJust(fields, kv.Field{
- K: "msg", V: fmt.Sprintf(s, a...),
- })
+ // If no args, use placeholders.
+ if msg == "" && len(args) > 0 {
+ const argstr = `%v%v%v%v%v%v%v%v%v%v` +
+ `%v%v%v%v%v%v%v%v%v%v` +
+ `%v%v%v%v%v%v%v%v%v%v` +
+ `%v%v%v%v%v%v%v%v%v%v`
+ msg = argstr[:2*len(args)]
}
- // Append formatted fields to log buffer.
- kv.Fields(fields).AppendFormat(buf, false)
+ if msg != "" {
+ // Format the message string.
+ msg = fmt.Sprintf(msg, args...)
+ }
+ // Append formatted
+ // entry to buffer.
+ appendFormat(buf,
+ now,
+ pcs[0],
+ lvl,
+ fields,
+ msg,
+ )
+
+ // Ensure a final new-line char.
if buf.B[len(buf.B)-1] != '\n' {
- // Append a final newline
buf.B = append(buf.B, '\n')
}
@@ -442,9 +340,8 @@ func logf(ctx context.Context, depth int, lvl LEVEL, fields []kv.Field, s string
logsys(lvl, buf.String())
}
- // Write to log and release
+ // Write to output file.
_, _ = out.Write(buf.B)
- putBuf(buf)
}
// logsys will log given msg at given severity to the syslog.
@@ -467,27 +364,3 @@ func logsys(lvl LEVEL, msg string) {
_ = sysout.Crit(msg)
}
}
-
-// args returns an args format string of format '%v' * count.
-func args(count int) string {
- const args = `%v%v%v%v%v%v%v%v%v%v` +
- `%v%v%v%v%v%v%v%v%v%v` +
- `%v%v%v%v%v%v%v%v%v%v` +
- `%v%v%v%v%v%v%v%v%v%v`
-
- // Use predetermined args str
- if count < len(args) {
- return args[:count*2]
- }
-
- // Allocate buffer of needed len
- var buf strings.Builder
- buf.Grow(count * 2)
-
- // Manually build an args str
- for i := 0; i < count; i++ {
- buf.WriteString(`%v`)
- }
-
- return buf.String()
-}
diff --git a/internal/log/pool.go b/internal/log/pool.go
index 3396fa244..1b45ed61f 100644
--- a/internal/log/pool.go
+++ b/internal/log/pool.go
@@ -23,22 +23,22 @@ import (
"codeberg.org/gruf/go-byteutil"
)
-// bufPool provides a memory pool of log buffers.
-var bufPool = sync.Pool{
- New: func() any {
- return &byteutil.Buffer{
- B: make([]byte, 0, 512),
- }
- },
-}
+// bufPool provides memory
+// pool of log buffers.
+var bufPool sync.Pool
// getBuf acquires a buffer from memory pool.
func getBuf() *byteutil.Buffer {
buf, _ := bufPool.Get().(*byteutil.Buffer)
+ if buf == nil {
+ buf = new(byteutil.Buffer)
+ buf.B = make([]byte, 0, 512)
+ }
return buf
}
-// putBuf places (after resetting) buffer back in memory pool, dropping if capacity too large.
+// putBuf places (after resetting) buffer back in
+// memory pool, dropping if capacity too large.
func putBuf(buf *byteutil.Buffer) {
if buf.Cap() > int(^uint16(0)) {
return // drop large buffer
diff --git a/scripts/build.sh b/scripts/build.sh
index b22861240..c6931e09d 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -6,7 +6,7 @@ set -e
log_exec() { echo "$ ${*}"; "$@"; }
# Grab environment variables and set defaults + requirements.
-GO_BUILDTAGS="${GO_BUILDTAGS-} netgo osusergo static_build kvformat timetzdata"
+GO_BUILDTAGS="${GO_BUILDTAGS-} netgo osusergo static_build timetzdata"
GO_LDFLAGS="${GO_LDFLAGS-} -s -w -extldflags '-static' -X 'main.Version=${VERSION:-$(git describe --tags --abbrev=0)}'"
GO_GCFLAGS=${GO_GCFLAGS-}
@@ -15,7 +15,6 @@ GO_GCFLAGS=${GO_GCFLAGS-}
GO_BUILDTAGS="${GO_BUILDTAGS} debugenv"
# Available Go build tags, with explanation, followed by benefits of enabling it:
-# - kvformat: enables prettier output of log fields (slightly better performance)
# - timetzdata: embed timezone database inside binary (allow setting local time inside Docker containers, at cost of 450KB)
# - nootel: disables compiling-in otel support (reduced binary size)
# - noerrcaller: disables caller function prefix in errors (slightly better performance, at cost of err readability)
diff --git a/scripts/test.sh b/scripts/test.sh
index 5e9271821..dbfdee884 100755
--- a/scripts/test.sh
+++ b/scripts/test.sh
@@ -7,7 +7,7 @@ set -e
# "./..." = all tests
# run tests with sqlite in-memory database
-GTS_DB_TYPE="sqlite" GTS_DB_ADDRESS=":memory:" go test -tags "netgo osusergo static_build kvformat" -count 1 ./...
+GTS_DB_TYPE="sqlite" GTS_DB_ADDRESS=":memory:" go test -tags "netgo osusergo static_build" -count 1 ./...
# run tests with postgres database at either GTS_DB_ADDRESS or default localhost
-GTS_DB_TYPE="postgres" GTS_DB_ADDRESS="${GTS_DB_ADDRESS:-localhost}" go test -tags "netgo osusergo static_build kvformat" -count 1 -p 1 ./...
+GTS_DB_TYPE="postgres" GTS_DB_ADDRESS="${GTS_DB_ADDRESS:-localhost}" go test -tags "netgo osusergo static_build" -count 1 -p 1 ./...
diff --git a/test/envparsing.sh b/test/envparsing.sh
index 4dba8f155..66baee482 100755
--- a/test/envparsing.sh
+++ b/test/envparsing.sh
@@ -137,6 +137,7 @@ EXPECT=$(cat << "EOF"
"local-only": false,
"log-client-ip": false,
"log-db-queries": true,
+ "log-format": "json",
"log-level": "info",
"log-timestamp-format": "banana",
"media-cleanup-every": 86400000000000,
@@ -223,6 +224,7 @@ OUTPUT=$(GTS_LOG_LEVEL='info' \
GTS_LOG_TIMESTAMP_FORMAT="banana" \
GTS_LOG_DB_QUERIES=true \
GTS_LOG_CLIENT_IP=false \
+GTS_LOG_FORMAT=json \
GTS_APPLICATION_NAME=gts \
GTS_LANDING_PAGE_USER=admin \
GTS_HOST=example.com \
diff --git a/testrig/config.go b/testrig/config.go
index fdc026e61..93d7fb554 100644
--- a/testrig/config.go
+++ b/testrig/config.go
@@ -57,7 +57,8 @@ func InitTestConfig() {
func testDefaults() config.Configuration {
return config.Configuration{
LogLevel: envStr("GTS_LOG_LEVEL", "error"),
- LogTimestampFormat: "02/01/2006 15:04:05.000",
+ LogFormat: envStr("GTS_LOG_FORMAT", "logfmt"),
+ LogTimestampFormat: envStr("GTS_LOG_TIMESTAMP_FORMAT", "02/01/2006 15:04:05.000"),
LogDbQueries: true,
ApplicationName: "gotosocial",
LandingPageUser: "",
diff --git a/testrig/util.go b/testrig/util.go
index bdd18ec29..b2602fbda 100644
--- a/testrig/util.go
+++ b/testrig/util.go
@@ -31,7 +31,7 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/messages"
"code.superseriousbusiness.org/gotosocial/internal/processing/workers"
"code.superseriousbusiness.org/gotosocial/internal/state"
- "codeberg.org/gruf/go-kv/v2"
+ "codeberg.org/gruf/go-kv/v2/format"
)
// Starts workers on the provided state using noop processing functions.
@@ -282,7 +282,6 @@ func WaitFor(condition func() bool) bool {
// dump returns debug output of 'v'.
func dump(v any) string {
- var kv kv.Field
- kv.V = v
- return kv.Value(false)
+ buf := format.Global.Append(nil, v, format.DefaultArgs())
+ return string(buf)
}
diff --git a/vendor/codeberg.org/gruf/go-caller/LICENSE b/vendor/codeberg.org/gruf/go-caller/LICENSE
new file mode 100644
index 000000000..d6f08d0ab
--- /dev/null
+++ b/vendor/codeberg.org/gruf/go-caller/LICENSE
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) gruf
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/codeberg.org/gruf/go-caller/README.md b/vendor/codeberg.org/gruf/go-caller/README.md
new file mode 100644
index 000000000..faae29e18
--- /dev/null
+++ b/vendor/codeberg.org/gruf/go-caller/README.md
@@ -0,0 +1,3 @@
+# go-caller
+
+runtime-cached Go calling function names per PC, useful in shaving time from outputting log entries.
\ No newline at end of file
diff --git a/vendor/codeberg.org/gruf/go-caller/caller.go b/vendor/codeberg.org/gruf/go-caller/caller.go
new file mode 100644
index 000000000..b07e05169
--- /dev/null
+++ b/vendor/codeberg.org/gruf/go-caller/caller.go
@@ -0,0 +1,136 @@
+package caller
+
+import (
+ "runtime"
+ "strings"
+ "sync/atomic"
+)
+
+var (
+ // callerCache caches PC values to string names.
+ // note this may be a little slower than Caller()
+ // calls on startup, but after all PCs are cached
+ // this should be ~3x faster + less GC overhead.
+ //
+ // see the following benchmark:
+ // goos: linux
+ // goarch: amd64
+ // pkg: codeberg.org/gruf/go-caller
+ // cpu: AMD Ryzen 7 7840U w/ Radeon 780M Graphics
+ // BenchmarkCallerCache
+ // BenchmarkCallerCache-16 16796982 66.19 ns/op 24 B/op 3 allocs/op
+ // BenchmarkNoCallerCache
+ // BenchmarkNoCallerCache-16 5486168 219.9 ns/op 744 B/op 6 allocs/op
+ callerCache atomic.Pointer[map[uintptr]string]
+
+ // stringCache caches strings to minimise string memory use
+ // by ensuring only 1 instance of the same func name string.
+ stringCache atomic.Pointer[map[string]string]
+)
+
+// Clear will empty the global caller PC -> func names cache.
+func Clear() { callerCache.Store(nil); stringCache.Store(nil) }
+
+// Name returns the calling function name for given
+// program counter, formatted to be useful for logging.
+func Name(pc uintptr) string {
+
+ // Get frame iterator for program counter.
+ frames := runtime.CallersFrames([]uintptr{pc})
+ if frames == nil {
+ return "???"
+ }
+
+ // Get func name from frame.
+ frame, _ := frames.Next()
+ name := frame.Function
+ if name == "" {
+ return "???"
+ }
+
+ // Drop all but package and function name, no path.
+ if idx := strings.LastIndex(name, "/"); idx >= 0 {
+ name = name[idx+1:]
+ }
+
+ const params = `[...]`
+
+ // Drop any function generic type parameter markers.
+ if idx := strings.Index(name, params); idx >= 0 {
+ name = name[:idx] + name[idx+len(params):]
+ }
+
+ return name
+}
+
+// Get will return calling func information for given PC value,
+// caching func names by their PC values to reduce calls to Caller().
+func Get(pc uintptr) string {
+ var cache map[uintptr]string
+ for {
+ // Load caller cache map.
+ ptr := callerCache.Load()
+
+ if ptr != nil {
+ // Look for stored name.
+ name, ok := (*ptr)[pc]
+ if ok {
+ return name
+ }
+
+ // Make a clone of existing caller cache map.
+ cache = make(map[uintptr]string, len(*ptr)+1)
+ for key, value := range *ptr {
+ cache[key] = value
+ }
+ } else {
+ // Allocate new caller cache map.
+ cache = make(map[uintptr]string, 1)
+ }
+
+ // Calculate caller
+ // name for PC value.
+ name := Name(pc)
+ name = getString(name)
+
+ // Store in map.
+ cache[pc] = name
+
+ // Attempt to update caller cache map pointer.
+ if callerCache.CompareAndSwap(ptr, &cache) {
+ return name
+ }
+ }
+}
+
+func getString(key string) string {
+ var cache map[string]string
+ for {
+ // Load string cache map.
+ ptr := stringCache.Load()
+
+ if ptr != nil {
+ // Check for existing string.
+ if str, ok := (*ptr)[key]; ok {
+ return str
+ }
+
+ // Make a clone of existing string cache map.
+ cache = make(map[string]string, len(*ptr)+1)
+ for key, value := range *ptr {
+ cache[key] = value
+ }
+ } else {
+ // Allocate new string cache map.
+ cache = make(map[string]string, 1)
+ }
+
+ // Store this str.
+ cache[key] = key
+
+ // Attempt to update string cache map pointer.
+ if stringCache.CompareAndSwap(ptr, &cache) {
+ return key
+ }
+ }
+}
diff --git a/vendor/codeberg.org/gruf/go-kv/v2/field_format.go b/vendor/codeberg.org/gruf/go-kv/v2/field_format.go
index 8085d4d01..f5db0f123 100644
--- a/vendor/codeberg.org/gruf/go-kv/v2/field_format.go
+++ b/vendor/codeberg.org/gruf/go-kv/v2/field_format.go
@@ -8,8 +8,6 @@ import (
"codeberg.org/gruf/go-kv/v2/format"
)
-var formatter format.Formatter
-
var argsDefault = format.DefaultArgs()
var argsVerbose = func() format.Args {
@@ -29,7 +27,7 @@ func (f Field) AppendFormat(buf *byteutil.Buffer, vbose bool) {
}
AppendQuoteString(buf, f.K)
buf.WriteByte('=')
- buf.B = formatter.Append(buf.B, f.V, args)
+ buf.B = format.Global.Append(buf.B, f.V, args)
}
// Value returns the formatted value string of this Field.
@@ -41,6 +39,6 @@ func (f Field) Value(vbose bool) string {
args = argsDefault
}
buf := make([]byte, 0, bufsize/2)
- buf = formatter.Append(buf, f.V, args)
+ buf = format.Global.Append(buf, f.V, args)
return byteutil.B2S(buf)
}
diff --git a/vendor/codeberg.org/gruf/go-kv/v2/format/abi.go b/vendor/codeberg.org/gruf/go-kv/v2/format/abi.go
index 815660a5e..b68d3cb73 100644
--- a/vendor/codeberg.org/gruf/go-kv/v2/format/abi.go
+++ b/vendor/codeberg.org/gruf/go-kv/v2/format/abi.go
@@ -1,4 +1,4 @@
-//go:build go1.24 && !go1.25
+//go:build go1.24 && !go1.26
package format
@@ -39,26 +39,36 @@ type abi_EmptyInterface struct {
Data unsafe.Pointer
}
+// abi_NonEmptyInterface is a copy of the memory layout of abi.NonEmptyInterface{},
+// which is to say also the memory layout of any interface containing method(s).
+//
+// see: go/src/internal/abi/iface.go on 1.25+
+// see: go/src/reflect/value.go on 1.24
+type abi_NonEmptyInterface struct {
+ ITab uintptr
+ Data unsafe.Pointer
+}
+
// see: go/src/internal/abi/type.go Type.Kind()
func abi_Type_Kind(t reflect.Type) uint8 {
- iface := (*reflect_nonEmptyInterface)(unsafe.Pointer(&t))
- atype := (*abi_Type)(unsafe.Pointer(iface.word))
+ iface := (*abi_NonEmptyInterface)(unsafe.Pointer(&t))
+ atype := (*abi_Type)(unsafe.Pointer(iface.Data))
return atype.Kind_ & abi_KindMask
}
// see: go/src/internal/abi/type.go Type.IfaceIndir()
func abi_Type_IfaceIndir(t reflect.Type) bool {
- iface := (*reflect_nonEmptyInterface)(unsafe.Pointer(&t))
- atype := (*abi_Type)(unsafe.Pointer(iface.word))
+ iface := (*abi_NonEmptyInterface)(unsafe.Pointer(&t))
+ atype := (*abi_Type)(unsafe.Pointer(iface.Data))
return atype.Kind_&abi_KindDirectIface == 0
}
-// pack_iface packs a new reflect.nonEmptyInterface{} using shielded itab
-// pointer and data (word) pointer, returning a pointer for caller casting.
+// pack_iface packs a new reflect.nonEmptyInterface{} using shielded
+// itab and data pointer, returning a pointer for caller casting.
func pack_iface(itab uintptr, word unsafe.Pointer) unsafe.Pointer {
- return unsafe.Pointer(&reflect_nonEmptyInterface{
- itab: itab,
- word: word,
+ return unsafe.Pointer(&abi_NonEmptyInterface{
+ ITab: itab,
+ Data: word,
})
}
@@ -68,8 +78,8 @@ func pack_iface(itab uintptr, word unsafe.Pointer) unsafe.Pointer {
// this is useful for later calls to pack_iface for known type.
func get_iface_ITab[I any](t reflect.Type) uintptr {
s := reflect.New(t).Elem().Interface().(I)
- i := (*reflect_nonEmptyInterface)(unsafe.Pointer(&s))
- return i.itab
+ i := (*abi_NonEmptyInterface)(unsafe.Pointer(&s))
+ return i.ITab
}
// unpack_eface returns the .Data portion of an abi.EmptyInterface{}.
@@ -162,15 +172,6 @@ func reflect_map_elem_flags(elemType reflect.Type) reflect_flag {
return reflect_flag(abi_Type_Kind(elemType))
}
-// reflect_nonEmptyInterface is a copy of the memory layout of reflect.nonEmptyInterface,
-// which is also to say the memory layout of any non-empty (i.e. w/ method) interface.
-//
-// see: go/src/reflect/value.go
-type reflect_nonEmptyInterface struct {
- itab uintptr
- word unsafe.Pointer
-}
-
// reflect_Value is a copy of the memory layout of reflect.Value{}.
//
// see: go/src/reflect/value.go
@@ -190,7 +191,7 @@ func init() {
// as the reflect.nonEmptyInterface{}, which itself will be a pointer
// to the actual abi.Type{} that this reflect.Type{} is wrapping.
func reflect_type_data(t reflect.Type) unsafe.Pointer {
- return (*reflect_nonEmptyInterface)(unsafe.Pointer(&t)).word
+ return (*abi_NonEmptyInterface)(unsafe.Pointer(&t)).Data
}
// build_reflect_value manually builds a reflect.Value{} by setting the internal field members.
diff --git a/vendor/codeberg.org/gruf/go-kv/v2/format/format.go b/vendor/codeberg.org/gruf/go-kv/v2/format/format.go
index 2ab62eb06..69edcb597 100644
--- a/vendor/codeberg.org/gruf/go-kv/v2/format/format.go
+++ b/vendor/codeberg.org/gruf/go-kv/v2/format/format.go
@@ -8,6 +8,9 @@ import (
"unsafe"
)
+// Global formatter instance.
+var Global Formatter
+
// FormatFunc defines a function capable of formatting
// the value contained in State{}.P, based on args in
// State{}.A, storing the result in buffer State{}.B.
@@ -48,7 +51,7 @@ const ringsz = 16
// ptr_ring is a ring buffer of pointers,
// purposely stored as uintptrs as all we
-// need them for is value comparisons and
+// need them for is integer comparisons and
// we don't want to hold-up the GC.
type ptr_ring struct {
p [ringsz]uintptr
@@ -296,38 +299,60 @@ func (fmt *Formatter) get(t typenode) (fn FormatFunc) {
func (fmt *Formatter) getInterfaceType(t typenode) FormatFunc {
if t.rtype.NumMethod() == 0 {
return func(s *State) {
+ // Unpack empty interface.
eface := *(*any)(s.P)
s.P = unpack_eface(eface)
+
+ // Get reflected type information.
rtype := reflect.TypeOf(eface)
if rtype == nil {
appendNil(s)
return
}
+
+ // Check for ptr recursion.
if s.ifaces.contains(s.P) {
getPointerType(t)(s)
return
}
+
+ // Store value ptr.
s.ifaces.set(s.P)
+
+ // Wrap in our typenode for before load.
flags := reflect_iface_elem_flags(rtype)
t := new_typenode(rtype, flags)
+
+ // Load + pass to func.
fmt.loadOrStore(t)(s)
}
} else {
return func(s *State) {
+ // Unpack interface-with-method ptr.
iface := *(*interface{ M() })(s.P)
s.P = unpack_eface(iface)
+
+ // Get reflected type information.
rtype := reflect.TypeOf(iface)
if rtype == nil {
appendNil(s)
return
}
+
+ // Check for ptr recursion.
if s.ifaces.contains(s.P) {
getPointerType(t)(s)
return
}
+
+ // Store value ptr.
s.ifaces.set(s.P)
+
+ // Wrap in our typenode for before load.
flags := reflect_iface_elem_flags(rtype)
t := new_typenode(rtype, flags)
+
+ // Load + pass to func.
fmt.loadOrStore(t)(s)
}
}
@@ -360,14 +385,11 @@ func getIntType(t typenode) FormatFunc {
switch {
case s.A.AsNumber():
// fallthrough
- case s.A.AsQuotedText():
- s.B = strconv.AppendQuoteRune(s.B, *(*rune)(s.P))
- return
case s.A.AsQuotedASCII():
s.B = strconv.AppendQuoteRuneToASCII(s.B, *(*rune)(s.P))
return
- case s.A.AsText():
- s.B = AppendEscapeRune(s.B, *(*rune)(s.P))
+ case s.A.AsText() || s.A.AsQuotedText():
+ s.B = strconv.AppendQuoteRune(s.B, *(*rune)(s.P))
return
}
appendInt(s, int64(*(*int32)(s.P)))
@@ -388,12 +410,9 @@ func getUintType(t typenode) FormatFunc {
switch {
case s.A.AsNumber():
// fallthrough
- case s.A.AsQuotedText() || s.A.AsQuotedASCII():
+ case s.A.AsText() || s.A.AsQuotedText() || s.A.AsQuotedASCII():
s.B = AppendQuoteByte(s.B, *(*byte)(s.P))
return
- case s.A.AsText():
- s.B = AppendEscapeByte(s.B, *(*byte)(s.P))
- return
}
appendUint(s, uint64(*(*uint8)(s.P)))
})
@@ -468,10 +487,17 @@ func with_typestr_ptrs(t typenode, fn FormatFunc) FormatFunc {
if fn == nil {
panic("nil func")
}
+
+ // Check for type wrapping.
if !t.needs_typestr() {
return fn
}
+
+ // Get type string with pointers.
typestr := t.typestr_with_ptrs()
+
+ // Wrap format func to include
+ // type information when needed.
return func(s *State) {
if s.A.WithType() {
s.B = append(s.B, "("+typestr+")("...)
@@ -485,7 +511,7 @@ func with_typestr_ptrs(t typenode, fn FormatFunc) FormatFunc {
func appendString(s *State, v string) {
switch {
- case s.A.Logfmt() || s.A.WithType():
+ case s.A.WithType():
if len(v) > SingleTermLine || !IsSafeASCII(v) {
// Requires quoting AND escaping
s.B = strconv.AppendQuote(s.B, v)
@@ -494,8 +520,22 @@ func appendString(s *State, v string) {
s.B = append(s.B, '"')
s.B = AppendEscape(s.B, v)
s.B = append(s.B, '"')
- } else if s.A.WithType() ||
- len(v) == 0 || ContainsSpaceOrTab(v) {
+ } else {
+ // All else, needs quotes
+ s.B = append(s.B, '"')
+ s.B = append(s.B, v...)
+ s.B = append(s.B, '"')
+ }
+ case s.A.Logfmt():
+ if len(v) > SingleTermLine || !IsSafeASCII(v) {
+ // Requires quoting AND escaping
+ s.B = strconv.AppendQuote(s.B, v)
+ } else if ContainsDoubleQuote(v) {
+ // Contains double quotes, needs escaping
+ s.B = append(s.B, '"')
+ s.B = AppendEscape(s.B, v)
+ s.B = append(s.B, '"')
+ } else if len(v) == 0 || ContainsSpaceOrTab(v) {
// Contains space / empty, needs quotes
s.B = append(s.B, '"')
s.B = append(s.B, v...)
@@ -515,75 +555,114 @@ func appendString(s *State, v string) {
func appendInt(s *State, v int64) {
args := s.A.Int
+
+ // Set argument defaults.
if args == zeroArgs.Int {
args = defaultArgs.Int
}
+
+ // Add any padding.
if args.Pad > 0 {
const zeros = `00000000000000000000`
if args.Pad > len(zeros) {
panic("cannot pad > " + zeros)
}
+
if v == 0 {
s.B = append(s.B, zeros[:args.Pad]...)
return
}
+
+ // Get absolute.
abs := abs64(v)
+
+ // Get number of required chars.
chars := int(v / int64(args.Base))
if v%int64(args.Base) != 0 {
chars++
}
+
if abs != v {
+ // If this is a negative value,
+ // prepend minus ourselves and
+ // set value as the absolute.
s.B = append(s.B, '-')
v = abs
}
- if n := args.Pad - chars; n > 0 {
- s.B = append(s.B, zeros[:n]...)
- }
+
+ // Prepend required zeros.
+ n := args.Pad - chars
+ s.B = append(s.B, zeros[:n]...)
}
+
+ // Append value as signed integer w/ args.
s.B = strconv.AppendInt(s.B, v, args.Base)
}
func appendUint(s *State, v uint64) {
args := s.A.Int
+
+ // Set argument defaults.
if args == zeroArgs.Int {
args = defaultArgs.Int
}
+
+ // Add any padding.
if args.Pad > 0 {
const zeros = `00000000000000000000`
if args.Pad > len(zeros) {
panic("cannot pad > " + zeros)
}
+
if v == 0 {
s.B = append(s.B, zeros[:args.Pad]...)
return
}
+
+ // Get number of required chars.
chars := int(v / uint64(args.Base))
if v%uint64(args.Base) != 0 {
chars++
}
- if n := args.Pad - chars; n > 0 {
- s.B = append(s.B, zeros[:n]...)
- }
+
+ // Prepend required zeros.
+ n := args.Pad - chars
+ s.B = append(s.B, zeros[:n]...)
}
+
+ // Append value as unsigned integer w/ args.
s.B = strconv.AppendUint(s.B, v, args.Base)
}
func appendFloat(s *State, v float64, bits int) {
args := s.A.Float
+
+ // Set argument defaults.
if args == zeroArgs.Float {
args = defaultArgs.Float
}
- s.B = strconv.AppendFloat(s.B, float64(v), args.Fmt, args.Prec, bits)
+
+ // Append value as float${bit} w/ args.
+ s.B = strconv.AppendFloat(s.B, float64(v),
+ args.Fmt, args.Prec, bits)
}
func appendComplex(s *State, r, i float64, bits int) {
args := s.A.Complex
+
+ // Set argument defaults.
if args == zeroArgs.Complex {
args = defaultArgs.Complex
}
- s.B = strconv.AppendFloat(s.B, float64(r), args.Real.Fmt, args.Real.Prec, bits)
+
+ // Append real value as float${bit} w/ args.
+ s.B = strconv.AppendFloat(s.B, float64(r),
+ args.Real.Fmt, args.Real.Prec, bits)
s.B = append(s.B, '+')
- s.B = strconv.AppendFloat(s.B, float64(i), args.Imag.Fmt, args.Imag.Prec, bits)
+
+ // Append imag value as float${bit} w/ args.
+ s.B = strconv.AppendFloat(s.B, float64(i),
+ args.Imag.Fmt, args.Imag.Prec, bits)
s.B = append(s.B, 'i')
}
diff --git a/vendor/codeberg.org/gruf/go-kv/v2/format/methods.go b/vendor/codeberg.org/gruf/go-kv/v2/format/methods.go
index 7c1795771..fcbffd3a4 100644
--- a/vendor/codeberg.org/gruf/go-kv/v2/format/methods.go
+++ b/vendor/codeberg.org/gruf/go-kv/v2/format/methods.go
@@ -45,7 +45,7 @@ func getInterfaceStringerType(t typenode) FormatFunc {
case true:
return with_typestr_ptrs(t, func(s *State) {
s.P = *(*unsafe.Pointer)(s.P)
- if s.P == nil || (*reflect_nonEmptyInterface)(s.P).word == nil {
+ if s.P == nil || (*abi_NonEmptyInterface)(s.P).Data == nil {
appendNil(s)
return
}
@@ -54,7 +54,7 @@ func getInterfaceStringerType(t typenode) FormatFunc {
})
case false:
return with_typestr_ptrs(t, func(s *State) {
- if s.P == nil || (*reflect_nonEmptyInterface)(s.P).word == nil {
+ if s.P == nil || (*abi_NonEmptyInterface)(s.P).Data == nil {
appendNil(s)
return
}
@@ -102,7 +102,7 @@ func getInterfaceErrorType(t typenode) FormatFunc {
case true:
return with_typestr_ptrs(t, func(s *State) {
s.P = *(*unsafe.Pointer)(s.P)
- if s.P == nil || (*reflect_nonEmptyInterface)(s.P).word == nil {
+ if s.P == nil || (*abi_NonEmptyInterface)(s.P).Data == nil {
appendNil(s)
return
}
@@ -111,7 +111,7 @@ func getInterfaceErrorType(t typenode) FormatFunc {
})
case false:
return with_typestr_ptrs(t, func(s *State) {
- if s.P == nil || (*reflect_nonEmptyInterface)(s.P).word == nil {
+ if s.P == nil || (*abi_NonEmptyInterface)(s.P).Data == nil {
appendNil(s)
return
}
diff --git a/vendor/codeberg.org/gruf/go-kv/v2/util.go b/vendor/codeberg.org/gruf/go-kv/v2/util.go
index 90fba59de..5939ce844 100644
--- a/vendor/codeberg.org/gruf/go-kv/v2/util.go
+++ b/vendor/codeberg.org/gruf/go-kv/v2/util.go
@@ -17,8 +17,8 @@ func AppendQuoteString(buf *byteutil.Buffer, str string) {
return
case len(str) == 1:
- // Append escaped single byte.
- buf.B = format.AppendEscapeByte(buf.B, str[0])
+ // Append quoted escaped single byte.
+ buf.B = format.AppendQuoteByte(buf.B, str[0])
return
case len(str) > format.SingleTermLine || !format.IsSafeASCII(str):
@@ -62,7 +62,7 @@ func AppendQuoteValue(buf *byteutil.Buffer, str string) {
return
case len(str) == 1:
- // Append quoted single byte.
+ // Append quoted escaped single byte.
buf.B = format.AppendQuoteByte(buf.B, str[0])
return
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 67c980943..cb9819e78 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -232,6 +232,9 @@ codeberg.org/gruf/go-byteutil
codeberg.org/gruf/go-cache/v3
codeberg.org/gruf/go-cache/v3/simple
codeberg.org/gruf/go-cache/v3/ttl
+# codeberg.org/gruf/go-caller v0.0.0-20250806133437-db8d0b1f71cf
+## explicit; go 1.24.5
+codeberg.org/gruf/go-caller
# codeberg.org/gruf/go-debug v1.3.0
## explicit; go 1.16
codeberg.org/gruf/go-debug
@@ -255,7 +258,7 @@ codeberg.org/gruf/go-iotools
## explicit; go 1.20
codeberg.org/gruf/go-kv
codeberg.org/gruf/go-kv/format
-# codeberg.org/gruf/go-kv/v2 v2.0.3
+# codeberg.org/gruf/go-kv/v2 v2.0.5
## explicit; go 1.24
codeberg.org/gruf/go-kv/v2
codeberg.org/gruf/go-kv/v2/format