[feature + performance] add JSON logging format (#4355)

# Description

Adds JSON logging as an optional alternative log output format. In the process this moves our log formatting itself into a separate subpkg to make it more easily modular, and improves caller name getting with some calling function name caching.

## Checklist

- [x] I/we have read the [GoToSocial contribution guidelines](https://codeberg.org/superseriousbusiness/gotosocial/src/branch/main/CONTRIBUTING.md).
- [x] I/we have discussed the proposed changes already, either in an issue on the repository, or in the Matrix chat.
- [x] I/we have not leveraged AI to create the proposed changes.
- [x] I/we have performed a self-review of added code.
- [x] I/we have written code that is legible and maintainable by others.
- [x] I/we have commented the added code, particularly in hard-to-understand areas.
- [x] I/we have made any necessary changes to documentation.
- [ ] I/we have added tests that cover new code.
- [x] I/we have run tests and they pass locally with the changes.
- [x] I/we have run `go fmt ./...` and `golangci-lint run`.

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4355
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2025-08-09 16:23:00 +02:00 committed by tobi
commit 7af9117e0d
37 changed files with 1070 additions and 439 deletions

View file

@ -64,11 +64,17 @@ func preRun(a preRunArgs) error {
// context, after initializing any last-minute things like loggers etc. // context, after initializing any last-minute things like loggers etc.
func run(ctx context.Context, action action.GTSAction) error { func run(ctx context.Context, action action.GTSAction) error {
log.SetTimeFormat(config.GetLogTimestampFormat()) 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 { if err := log.ParseLevel(config.GetLogLevel()); err != nil {
return fmt.Errorf("error parsing log level: %w", err) 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() { if config.GetSyslogEnabled() {
// Enable logging to syslog // Enable logging to syslog
if err := log.EnableSyslog( if err := log.EnableSyslog(

View file

@ -26,6 +26,19 @@ log-db-queries: false
# Default: true # Default: true
log-client-ip: 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. # String. Format to use for the timestamp in log lines.
# If set to the empty string, the timestamp will be # If set to the empty string, the timestamp will be
# ommitted from the logs entirely. # ommitted from the logs entirely.

View file

@ -36,6 +36,19 @@ log-db-queries: false
# Default: true # Default: true
log-client-ip: 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. # String. Format to use for the timestamp in log lines.
# If set to the empty string, the timestamp will be # If set to the empty string, the timestamp will be
# ommitted from the logs entirely. # ommitted from the logs entirely.

7
go.mod
View file

@ -1,8 +1,6 @@
module code.superseriousbusiness.org/gotosocial module code.superseriousbusiness.org/gotosocial
go 1.24 go 1.24.5
toolchain go1.24.3
// Replace go-swagger with our version that fixes (ours particularly) use of Go1.23 // 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 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-bytesize v1.0.3
codeberg.org/gruf/go-byteutil v1.3.0 codeberg.org/gruf/go-byteutil v1.3.0
codeberg.org/gruf/go-cache/v3 v3.6.1 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-debug v1.3.0
codeberg.org/gruf/go-errors/v2 v2.3.2 codeberg.org/gruf/go-errors/v2 v2.3.2
codeberg.org/gruf/go-fastcopy v1.1.3 codeberg.org/gruf/go-fastcopy v1.1.3
codeberg.org/gruf/go-ffmpreg v0.6.8 codeberg.org/gruf/go-ffmpreg v0.6.8
codeberg.org/gruf/go-iotools v0.0.0-20240710125620-934ae9c654cf 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-list v0.0.0-20240425093752-494db03d641f
codeberg.org/gruf/go-mempool v0.0.0-20240507125005-cef10d64a760 codeberg.org/gruf/go-mempool v0.0.0-20240507125005-cef10d64a760
codeberg.org/gruf/go-mutexes v1.5.2 codeberg.org/gruf/go-mutexes v1.5.2

6
go.sum generated
View file

@ -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-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 h1:sY1XhYeskjZAuYeMm5R0o4Qymru5taNbzmZPSn1oXLE=
codeberg.org/gruf/go-cache/v3 v3.6.1/go.mod h1:JUNjc4E8gRccn3t+B99akxURFrU6NTDkvFVcwiZirnw= 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 h1:PIRxQiWUFKtGOGZFdZ3Y0pqyfI0Xr87j224IYe2snZs=
codeberg.org/gruf/go-debug v1.3.0/go.mod h1:N+vSy9uJBQgpQcJUqjctvqFz7tBHJf+S/PIjLILzpLg= 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= 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-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 h1:ttPf0NA8F79pDqBttSudPTVCZmGncumeNIxmeM9ztz0=
codeberg.org/gruf/go-kv v1.6.5/go.mod h1:c4PsGqw05bDScvISpK+d31SiDEpBorweCL50hsiK3dc= 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.5 h1:FuUAJcdWrj1jzySalGpe8sZSvBLr+LbvZiHMjt014s0=
codeberg.org/gruf/go-kv/v2 v2.0.3/go.mod h1:mNL6SrBnYGEyrx6Mh4E1tAdhO0+T9/1iBrPJxIwxY24= 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 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-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= codeberg.org/gruf/go-loosy v0.0.0-20231007123304-bb910d1ab5c4 h1:IXwfoU7f2whT6+JKIKskNl/hBlmWmnF1vZd84Eb3cyA=

View file

@ -97,11 +97,12 @@ func ExtractActivityData(activity pub.Activity, rawJSON map[string]any) ([]TypeO
func ExtractAccountables(arr []TypeOrIRI) ([]Accountable, []TypeOrIRI) { func ExtractAccountables(arr []TypeOrIRI) ([]Accountable, []TypeOrIRI) {
var accounts []Accountable var accounts []Accountable
for i := 0; i < len(arr); i++ { for i := 0; i < len(arr); {
elem := arr[i] elem := arr[i]
if elem.IsIRI() { if elem.IsIRI() {
// skip IRIs // skip IRIs
i++ // iter
continue continue
} }
@ -112,6 +113,7 @@ func ExtractAccountables(arr []TypeOrIRI) ([]Accountable, []TypeOrIRI) {
// Try cast AS type as Accountable. // Try cast AS type as Accountable.
account, ok := ToAccountable(t) account, ok := ToAccountable(t)
if !ok { if !ok {
i++ // iter
continue continue
} }
@ -130,11 +132,12 @@ func ExtractAccountables(arr []TypeOrIRI) ([]Accountable, []TypeOrIRI) {
func ExtractStatusables(arr []TypeOrIRI) ([]Statusable, []TypeOrIRI) { func ExtractStatusables(arr []TypeOrIRI) ([]Statusable, []TypeOrIRI) {
var statuses []Statusable var statuses []Statusable
for i := 0; i < len(arr); i++ { for i := 0; i < len(arr); {
elem := arr[i] elem := arr[i]
if elem.IsIRI() { if elem.IsIRI() {
// skip IRIs // skip IRIs
i++ // iter
continue continue
} }
@ -145,10 +148,11 @@ func ExtractStatusables(arr []TypeOrIRI) ([]Statusable, []TypeOrIRI) {
// Try cast AS type as Statusable. // Try cast AS type as Statusable.
status, ok := ToStatusable(t) status, ok := ToStatusable(t)
if !ok { if !ok {
i++ // iter
continue continue
} }
// Add casted Statusable type. // Append casted Statusable type.
statuses = append(statuses, status) statuses = append(statuses, status)
// Drop elem from slice. // Drop elem from slice.
@ -163,11 +167,12 @@ func ExtractStatusables(arr []TypeOrIRI) ([]Statusable, []TypeOrIRI) {
func ExtractPollOptionables(arr []TypeOrIRI) ([]PollOptionable, []TypeOrIRI) { func ExtractPollOptionables(arr []TypeOrIRI) ([]PollOptionable, []TypeOrIRI) {
var options []PollOptionable var options []PollOptionable
for i := 0; i < len(arr); i++ { for i := 0; i < len(arr); {
elem := arr[i] elem := arr[i]
if elem.IsIRI() { if elem.IsIRI() {
// skip IRIs // skip IRIs
i++ // iter
continue continue
} }
@ -178,6 +183,7 @@ func ExtractPollOptionables(arr []TypeOrIRI) ([]PollOptionable, []TypeOrIRI) {
// Try cast as PollOptionable. // Try cast as PollOptionable.
option, ok := ToPollOptionable(t) option, ok := ToPollOptionable(t)
if !ok { if !ok {
i++ // iter
continue continue
} }

View file

@ -55,6 +55,7 @@ func fieldtag(field, tag string) string {
// https://github.com/mvdan/gofumpt. // https://github.com/mvdan/gofumpt.
type Configuration struct { type Configuration struct {
LogLevel string `name:"log-level" usage:"Log level to run at: [trace, debug, info, warn, fatal]"` 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"` 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"` 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"` LogClientIP bool `name:"log-client-ip" usage:"Include the client IP in logs"`

View file

@ -29,6 +29,7 @@ import (
// if you use this, you will still need to set Host, and, if desired, ConfigPath. // if you use this, you will still need to set Host, and, if desired, ConfigPath.
var Defaults = Configuration{ var Defaults = Configuration{
LogLevel: "info", LogLevel: "info",
LogFormat: "logfmt",
LogTimestampFormat: "02/01/2006 15:04:05.000", LogTimestampFormat: "02/01/2006 15:04:05.000",
LogDbQueries: false, LogDbQueries: false,
ApplicationName: "gotosocial", ApplicationName: "gotosocial",

View file

@ -30,6 +30,7 @@ import (
const ( const (
LogLevelFlag = "log-level" LogLevelFlag = "log-level"
LogFormatFlag = "log-format"
LogTimestampFormatFlag = "log-timestamp-format" LogTimestampFormatFlag = "log-timestamp-format"
LogDbQueriesFlag = "log-db-queries" LogDbQueriesFlag = "log-db-queries"
LogClientIPFlag = "log-client-ip" LogClientIPFlag = "log-client-ip"
@ -226,6 +227,7 @@ const (
func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) { 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-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.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-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") 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 { 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-level"] = cfg.LogLevel
cfgmap["log-format"] = cfg.LogFormat
cfgmap["log-timestamp-format"] = cfg.LogTimestampFormat cfgmap["log-timestamp-format"] = cfg.LogTimestampFormat
cfgmap["log-db-queries"] = cfg.LogDbQueries cfgmap["log-db-queries"] = cfg.LogDbQueries
cfgmap["log-client-ip"] = cfg.LogClientIP 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 { if ival, ok := cfgmap["log-timestamp-format"]; ok {
var err error var err error
cfg.LogTimestampFormat, err = cast.ToStringE(ival) 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 // SetLogLevel safely sets the value for global configuration 'LogLevel' field
func SetLogLevel(v string) { global.SetLogLevel(v) } 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 // GetLogTimestampFormat safely fetches the Configuration value for state's 'LogTimestampFormat' field
func (st *ConfigState) GetLogTimestampFormat() (v string) { func (st *ConfigState) GetLogTimestampFormat() (v string) {
st.mutex.RLock() st.mutex.RLock()

View file

@ -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. // 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) { func (queryHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) {
// Get the DB query duration // Get the database query duration.
dur := time.Since(event.StartTime) dur := time.Since(event.StartTime)
switch { switch {
// Warn on slow database queries // Warn on slow queries.
case dur > time.Second: case dur > time.Second:
log.WithContext(ctx). log.WithContext(ctx).
WithFields(kv.Fields{ WithFields(kv.Fields{
{"duration", dur}, {"duration", dur},
{"query", event.Query}, {"query", event.Query},
}...).Warn("SLOW DATABASE QUERY") }...).
Warn("SLOW DATABASE QUERY")
// On trace, we log query information, // On trace log query info.
// manually crafting so DB query not escaped.
case log.Level() >= log.TRACE: 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},
}...)
} }
} }

View file

@ -20,6 +20,7 @@ package federatingdb
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/url" "net/url"
"code.superseriousbusiness.org/activity/streams" "code.superseriousbusiness.org/activity/streams"
@ -212,15 +213,23 @@ func getActivityContext(ctx context.Context) activityContext {
// lazy-serialization along with error output. // lazy-serialization along with error output.
type serialize struct{ item vocab.Type } 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 { func (s serialize) String() string {
m, err := ap.Serialize(s.item) m, err := ap.Serialize(s.item)
if err != nil { if err != nil {
return "!(error serializing item: " + err.Error() + ")" return "!(error serializing: " + err.Error() + ")"
} }
b, err := json.Marshal(m) b, err := json.Marshal(m)
if err != nil { if err != nil {
return "!(error json marshaling item: " + err.Error() + ")" return "!(error marshaling: " + err.Error() + ")"
} }
return byteutil.B2S(b) return byteutil.B2S(b)

View file

@ -24,6 +24,8 @@ import (
"fmt" "fmt"
"runtime" "runtime"
"strings" "strings"
"codeberg.org/gruf/go-caller"
) )
// Caller returns whether created errors will prepend calling function name. // 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. // newAt is the same as New() but allows specifying calldepth.
func newAt(calldepth int, msg string) error { func newAt(calldepth int, msg string) error {
return &cerror{ return &cerror{
c: caller(calldepth + 1), c: getCaller(calldepth + 1),
e: errors.New(msg), 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. // newfAt is the same as Newf() but allows specifying calldepth.
func newfAt(calldepth int, msgf string, args ...any) error { func newfAt(calldepth int, msgf string, args ...any) error {
return &cerror{ return &cerror{
c: caller(calldepth + 1), c: getCaller(calldepth + 1),
e: fmt.Errorf(msgf, args...), e: fmt.Errorf(msgf, args...),
} }
} }
// caller fetches the calling function name, skipping 'depth'. // getCaller fetches the calling function name, skipping 'depth'.
func caller(depth int) string { func getCaller(depth int) string {
var pcs [1]uintptr pcs := make([]uintptr, 1)
// Fetch calling function using calldepth // Fetch calling function at depth.
_ = runtime.Callers(depth, pcs[:]) _ = runtime.Callers(depth, pcs)
fn := runtime.FuncForPC(pcs[0])
if fn == nil { // Get cached calling func name.
return "" name := caller.Get(pcs[0])
}
// Get func name. // Drop package / everything but function name itself.
name := fn.Name()
// Drop everything but but function name itself
if idx := strings.LastIndexByte(name, '.'); idx >= 0 { if idx := strings.LastIndexByte(name, '.'); idx >= 0 {
name = name[idx+1:] 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 return name
} }

View file

@ -19,7 +19,8 @@ package log
import ( import (
"runtime" "runtime"
"strings"
"codeberg.org/gruf/go-caller"
) )
// Caller fetches the calling function name, skipping 'depth'. // Caller fetches the calling function name, skipping 'depth'.
@ -27,29 +28,6 @@ import (
//go:noinline //go:noinline
func Caller(depth int) string { func Caller(depth int) string {
pcs := make([]uintptr, 1) pcs := make([]uintptr, 1)
// Fetch calling func using depth.
_ = runtime.Callers(depth, pcs) _ = runtime.Callers(depth, pcs)
fn := runtime.FuncForPC(pcs[0]) return caller.Get(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
} }

View file

@ -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. // Trace will log formatted args as 'msg' field to the log at TRACE level.
//
//go:noinline
func (e Entry) Trace(a ...interface{}) { 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. // Tracef will log format string as 'msg' field to the log at TRACE level.
//
//go:noinline
func (e Entry) Tracef(s string, a ...interface{}) { 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. // Debug will log formatted args as 'msg' field to the log at DEBUG level.
//
//go:noinline
func (e Entry) Debug(a ...interface{}) { 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. // Debugf will log format string as 'msg' field to the log at DEBUG level.
//
//go:noinline
func (e Entry) Debugf(s string, a ...interface{}) { 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. // Info will log formatted args as 'msg' field to the log at INFO level.
//
//go:noinline
func (e Entry) Info(a ...interface{}) { 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. // Infof will log format string as 'msg' field to the log at INFO level.
//
//go:noinline
func (e Entry) Infof(s string, a ...interface{}) { 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. // Warn will log formatted args as 'msg' field to the log at WARN level.
//
//go:noinline
func (e Entry) Warn(a ...interface{}) { 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. // Warnf will log format string as 'msg' field to the log at WARN level.
//
//go:noinline
func (e Entry) Warnf(s string, a ...interface{}) { 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. // Error will log formatted args as 'msg' field to the log at ERROR level.
//
//go:noinline
func (e Entry) Error(a ...interface{}) { 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. // Errorf will log format string as 'msg' field to the log at ERROR level.
//
//go:noinline
func (e Entry) Errorf(s string, a ...interface{}) { 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. // Panic will log formatted args as 'msg' field to the log at PANIC level.
// This will then call panic causing the application to crash. // This will then call panic causing the application to crash.
//
//go:noinline
func (e Entry) Panic(a ...interface{}) { func (e Entry) Panic(a ...interface{}) {
defer panic(fmt.Sprint(a...)) 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. // Panicf will log format string as 'msg' field to the log at PANIC level.
// This will then call panic causing the application to crash. // This will then call panic causing the application to crash.
//
//go:noinline
func (e Entry) Panicf(s string, a ...interface{}) { func (e Entry) Panicf(s string, a ...interface{}) {
defer panic(fmt.Sprintf(s, a...)) 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. // Log will log formatted args as 'msg' field to the log at given level.
//
//go:noinline
func (e Entry) Log(lvl LEVEL, a ...interface{}) { 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. // 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{}) { 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. // Print will log formatted args to the stdout log output.
//
//go:noinline
func (e Entry) Print(a ...interface{}) { 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. // Printf will log format string to the stdout log output.
//
//go:noinline
func (e Entry) Printf(s string, a ...interface{}) { func (e Entry) Printf(s string, a ...interface{}) {
printf(3, e.kvs, s, a...) logf(e.ctx, UNSET, e.kvs, s, a...)
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
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...)
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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]
})
}

241
internal/log/format/json.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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,
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}

View file

@ -23,7 +23,8 @@ import (
"strings" "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 { func ParseLevel(str string) error {
switch strings.ToLower(str) { switch strings.ToLower(str) {
case "trace": case "trace":
@ -44,16 +45,22 @@ func ParseLevel(str string) error {
return nil return nil
} }
// EnableSyslog will enabling logging to the syslog at given address. // ParseFormat will parse the log format from
func EnableSyslog(proto, addr string) error { // given string and set appropriate formatter.
// Dial a connection to the syslog daemon func ParseFormat(str string) error {
writer, err := syslog.Dial(proto, addr, 0, "gotosocial") switch strings.ToLower(str) {
if err != nil { case "json":
return err SetJSON(true)
case "", "logfmt":
SetJSON(false)
default:
return fmt.Errorf("unknown log format: %q", str)
} }
// Set the syslog writer
sysout = writer
return nil 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
}

View file

@ -17,22 +17,18 @@
package log package log
// LEVEL defines a level of logging. import (
type LEVEL uint8 "code.superseriousbusiness.org/gotosocial/internal/log/level"
// 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)
) )
// CanLog returns whether an incoming log of 'lvl' can be logged against receiving level. type LEVEL = level.LEVEL
func (loglvl LEVEL) CanLog(lvl LEVEL) bool {
return loglvl > lvl const (
} PANIC = level.PANIC
ERROR = level.ERROR
WARN = level.WARN
INFO = level.INFO
DEBUG = level.DEBUG
TRACE = level.TRACE
UNSET = level.UNSET
)

View file

@ -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 <http://www.gnu.org/licenses/>.
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",
}

View file

@ -22,35 +22,31 @@ import (
"fmt" "fmt"
"log/syslog" "log/syslog"
"os" "os"
"strings" "runtime"
"time" "time"
"code.superseriousbusiness.org/gotosocial/internal/log/format"
"code.superseriousbusiness.org/gotosocial/internal/log/level"
"code.superseriousbusiness.org/gotosocial/internal/util/xslices" "code.superseriousbusiness.org/gotosocial/internal/util/xslices"
"codeberg.org/gruf/go-kv/v2" "codeberg.org/gruf/go-kv/v2"
) )
var ( var (
// loglvl is the currently // loglvl is the currently
// set logging output // set logging output.
loglvl LEVEL loglvl = level.UNSET
// lvlstrs is the lookup table // appendFormat stores log
// of all log levels to strings. // entry formatting function.
lvlstrs = [int(ALL) + 1]string{ appendFormat = (&format.Logfmt{
TRACE: "TRACE", Base: format.Base{TimeFormat: timefmt},
DEBUG: "DEBUG", }).Format
INFO: "INFO",
WARN: "WARN",
ERROR: "ERROR",
PANIC: "PANIC",
}
// syslog output, only set if enabled. // syslog output, only set if enabled.
sysout *syslog.Writer sysout *syslog.Writer
// timefmt is the logging time format used, which includes // timefmt is the logging time format used.
// the full field and required quoting timefmt = `02/01/2006 15:04:05.000`
timefmt = `timestamp="02/01/2006 15:04:05.000" `
// ctxhooks allows modifying log content based on context. // ctxhooks allows modifying log content based on context.
ctxhooks []func(context.Context, []kv.Field) []kv.Field 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) ctxhooks = append(ctxhooks, hook)
} }
// Level returns the currently set log // Level returns the currently set log.
func Level() LEVEL { func Level() LEVEL {
return loglvl return loglvl
} }
// SetLevel sets the max logging // SetLevel sets the max logging.
func SetLevel(lvl LEVEL) { func SetLevel(lvl LEVEL) {
loglvl = lvl loglvl = lvl
} }
@ -78,11 +74,20 @@ func TimeFormat() string {
// SetTimeFormat sets the timestamp format to the given string. // SetTimeFormat sets the timestamp format to the given string.
func SetTimeFormat(format string) { func SetTimeFormat(format string) {
if format == "" { timefmt = format
timefmt = format }
return
// 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. // New starts a new log entry.
@ -105,282 +110,166 @@ func WithFields(fields ...kv.Field) Entry {
return Entry{kvs: fields} 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. // Trace will log formatted args as 'msg' field to the log at TRACE level.
//
//go:noinline
func Trace(ctx context.Context, a ...interface{}) { 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. // 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{}) { 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. // 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{}) { 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. // TraceKVs will log key-value fields to the log at TRACE level.
//
//go:noinline
func TraceKVs(ctx context.Context, kvs ...kv.Field) { 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. // Debug will log formatted args as 'msg' field to the log at DEBUG level.
//
//go:noinline
func Debug(ctx context.Context, a ...interface{}) { 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. // 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{}) { 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. // 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{}) { 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. // DebugKVs will log key-value fields to the log at DEBUG level.
//
//go:noinline
func DebugKVs(ctx context.Context, kvs ...kv.Field) { 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. // Info will log formatted args as 'msg' field to the log at INFO level.
//
//go:noinline
func Info(ctx context.Context, a ...interface{}) { 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. // 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{}) { 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. // 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{}) { 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. // InfoKVs will log key-value fields to the log at INFO level.
//
//go:noinline
func InfoKVs(ctx context.Context, kvs ...kv.Field) { 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. // Warn will log formatted args as 'msg' field to the log at WARN level.
//
//go:noinline
func Warn(ctx context.Context, a ...interface{}) { 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. // 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{}) { 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. // 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{}) { 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. // WarnKVs will log key-value fields to the log at WARN level.
//
//go:noinline
func WarnKVs(ctx context.Context, kvs ...kv.Field) { 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. // Error will log formatted args as 'msg' field to the log at ERROR level.
//
//go:noinline
func Error(ctx context.Context, a ...interface{}) { 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. // 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{}) { 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. // 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{}) { 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. // ErrorKVs will log key-value fields to the log at ERROR level.
//
//go:noinline
func ErrorKVs(ctx context.Context, kvs ...kv.Field) { 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. // Panic will log formatted args as 'msg' field to the log at PANIC level.
// This will then call panic causing the application to crash. // This will then call panic causing the application to crash.
//
//go:noinline
func Panic(ctx context.Context, a ...interface{}) { func Panic(ctx context.Context, a ...interface{}) {
defer panic(fmt.Sprint(a...)) 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. // Panicf will log format string as 'msg' field to the log at PANIC level.
// This will then call panic causing the application to crash. // This will then call panic causing the application to crash.
//
//go:noinline
func Panicf(ctx context.Context, s string, a ...interface{}) { func Panicf(ctx context.Context, s string, a ...interface{}) {
defer panic(fmt.Sprintf(s, a...)) 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. // PanicKV will log the one key-value field to the log at PANIC level.
// This will then call panic causing the application to crash. // This will then call panic causing the application to crash.
//
//go:noinline
func PanicKV(ctx context.Context, key string, value interface{}) { func PanicKV(ctx context.Context, key string, value interface{}) {
defer panic(kv.Field{K: key, V: value}.String()) 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. // PanicKVs will log key-value fields to the log at PANIC level.
// This will then call panic causing the application to crash. // This will then call panic causing the application to crash.
//
//go:noinline
func PanicKVs(ctx context.Context, kvs ...kv.Field) { func PanicKVs(ctx context.Context, kvs ...kv.Field) {
defer panic(kv.Fields(kvs).String()) 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. // Log will log formatted args as 'msg' field to the log at given level.
// func Log(ctx context.Context, lvl LEVEL, a ...interface{}) { //nolint:revive
//go:noinline logf(ctx, lvl, nil, "", a...)
func Log(ctx context.Context, lvl LEVEL, a ...interface{}) {
logf(ctx, 3, lvl, nil, args(len(a)), a...)
} }
// Logf will log format string as 'msg' field to the log at given level. // Logf will log format string as 'msg' field to the log at given level.
// func Logf(ctx context.Context, lvl LEVEL, s string, a ...interface{}) { //nolint:revive
//go:noinline logf(ctx, lvl, nil, s, a...)
func Logf(ctx context.Context, lvl LEVEL, s string, a ...interface{}) {
logf(ctx, 3, lvl, nil, s, a...)
} }
// LogKV will log the one key-value field to the log at given level. // 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 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. // 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 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. // Print will log formatted args to the stdout log output.
//
//go:noinline
func Print(a ...interface{}) { 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. // Printf will log format string to the stdout log output.
//
//go:noinline
func Printf(s string, a ...interface{}) { func Printf(s string, a ...interface{}) {
printf(3, nil, s, a...) logf(context.Background(), UNSET, 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, "")
} }
//go:noinline //go:noinline
func printf(depth int, fields []kv.Field, s string, a ...interface{}) { func logf(ctx context.Context, lvl LEVEL, fields []kv.Field, msg string, args ...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{}) {
var out *os.File var out *os.File
// Check if enabled. // Check if enabled.
@ -396,25 +285,20 @@ func logf(ctx context.Context, depth int, lvl LEVEL, fields []kv.Field, s string
out = os.Stdout 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() buf := getBuf()
defer putBuf(buf)
// Append formatted timestamp according to `timefmt` if ctx != nil {
buf.B = time.Now().AppendFormat(buf.B, timefmt) // Ensure fields have space for context hooks.
fields = xslices.GrowJust(fields, len(ctxhooks))
// 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)
// Pass context through hooks. // Pass context through hooks.
for _, hook := range ctxhooks { for _, hook := range ctxhooks {
@ -422,18 +306,32 @@ func logf(ctx context.Context, depth int, lvl LEVEL, fields []kv.Field, s string
} }
} }
if s != "" { // If no args, use placeholders.
// Append message (if given) as final log field. if msg == "" && len(args) > 0 {
fields = xslices.AppendJust(fields, kv.Field{ const argstr = `%v%v%v%v%v%v%v%v%v%v` +
K: "msg", V: fmt.Sprintf(s, a...), `%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. if msg != "" {
kv.Fields(fields).AppendFormat(buf, false) // 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' { if buf.B[len(buf.B)-1] != '\n' {
// Append a final newline
buf.B = append(buf.B, '\n') 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()) logsys(lvl, buf.String())
} }
// Write to log and release // Write to output file.
_, _ = out.Write(buf.B) _, _ = out.Write(buf.B)
putBuf(buf)
} }
// logsys will log given msg at given severity to the syslog. // logsys will log given msg at given severity to the syslog.
@ -467,27 +364,3 @@ func logsys(lvl LEVEL, msg string) {
_ = sysout.Crit(msg) _ = 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()
}

View file

@ -23,22 +23,22 @@ import (
"codeberg.org/gruf/go-byteutil" "codeberg.org/gruf/go-byteutil"
) )
// bufPool provides a memory pool of log buffers. // bufPool provides memory
var bufPool = sync.Pool{ // pool of log buffers.
New: func() any { var bufPool sync.Pool
return &byteutil.Buffer{
B: make([]byte, 0, 512),
}
},
}
// getBuf acquires a buffer from memory pool. // getBuf acquires a buffer from memory pool.
func getBuf() *byteutil.Buffer { func getBuf() *byteutil.Buffer {
buf, _ := bufPool.Get().(*byteutil.Buffer) buf, _ := bufPool.Get().(*byteutil.Buffer)
if buf == nil {
buf = new(byteutil.Buffer)
buf.B = make([]byte, 0, 512)
}
return buf 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) { func putBuf(buf *byteutil.Buffer) {
if buf.Cap() > int(^uint16(0)) { if buf.Cap() > int(^uint16(0)) {
return // drop large buffer return // drop large buffer

View file

@ -6,7 +6,7 @@ set -e
log_exec() { echo "$ ${*}"; "$@"; } log_exec() { echo "$ ${*}"; "$@"; }
# Grab environment variables and set defaults + requirements. # 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_LDFLAGS="${GO_LDFLAGS-} -s -w -extldflags '-static' -X 'main.Version=${VERSION:-$(git describe --tags --abbrev=0)}'"
GO_GCFLAGS=${GO_GCFLAGS-} GO_GCFLAGS=${GO_GCFLAGS-}
@ -15,7 +15,6 @@ GO_GCFLAGS=${GO_GCFLAGS-}
GO_BUILDTAGS="${GO_BUILDTAGS} debugenv" GO_BUILDTAGS="${GO_BUILDTAGS} debugenv"
# Available Go build tags, with explanation, followed by benefits of enabling it: # 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) # - 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) # - nootel: disables compiling-in otel support (reduced binary size)
# - noerrcaller: disables caller function prefix in errors (slightly better performance, at cost of err readability) # - noerrcaller: disables caller function prefix in errors (slightly better performance, at cost of err readability)

View file

@ -7,7 +7,7 @@ set -e
# "./..." = all tests # "./..." = all tests
# run tests with sqlite in-memory database # 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 # 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 ./...

View file

@ -137,6 +137,7 @@ EXPECT=$(cat << "EOF"
"local-only": false, "local-only": false,
"log-client-ip": false, "log-client-ip": false,
"log-db-queries": true, "log-db-queries": true,
"log-format": "json",
"log-level": "info", "log-level": "info",
"log-timestamp-format": "banana", "log-timestamp-format": "banana",
"media-cleanup-every": 86400000000000, "media-cleanup-every": 86400000000000,
@ -223,6 +224,7 @@ OUTPUT=$(GTS_LOG_LEVEL='info' \
GTS_LOG_TIMESTAMP_FORMAT="banana" \ GTS_LOG_TIMESTAMP_FORMAT="banana" \
GTS_LOG_DB_QUERIES=true \ GTS_LOG_DB_QUERIES=true \
GTS_LOG_CLIENT_IP=false \ GTS_LOG_CLIENT_IP=false \
GTS_LOG_FORMAT=json \
GTS_APPLICATION_NAME=gts \ GTS_APPLICATION_NAME=gts \
GTS_LANDING_PAGE_USER=admin \ GTS_LANDING_PAGE_USER=admin \
GTS_HOST=example.com \ GTS_HOST=example.com \

View file

@ -57,7 +57,8 @@ func InitTestConfig() {
func testDefaults() config.Configuration { func testDefaults() config.Configuration {
return config.Configuration{ return config.Configuration{
LogLevel: envStr("GTS_LOG_LEVEL", "error"), 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, LogDbQueries: true,
ApplicationName: "gotosocial", ApplicationName: "gotosocial",
LandingPageUser: "", LandingPageUser: "",

View file

@ -31,7 +31,7 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/messages" "code.superseriousbusiness.org/gotosocial/internal/messages"
"code.superseriousbusiness.org/gotosocial/internal/processing/workers" "code.superseriousbusiness.org/gotosocial/internal/processing/workers"
"code.superseriousbusiness.org/gotosocial/internal/state" "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. // 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'. // dump returns debug output of 'v'.
func dump(v any) string { func dump(v any) string {
var kv kv.Field buf := format.Global.Append(nil, v, format.DefaultArgs())
kv.V = v return string(buf)
return kv.Value(false)
} }

9
vendor/codeberg.org/gruf/go-caller/LICENSE generated vendored Normal file
View file

@ -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.

3
vendor/codeberg.org/gruf/go-caller/README.md generated vendored Normal file
View file

@ -0,0 +1,3 @@
# go-caller
runtime-cached Go calling function names per PC, useful in shaving time from outputting log entries.

136
vendor/codeberg.org/gruf/go-caller/caller.go generated vendored Normal file
View file

@ -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
}
}
}

View file

@ -8,8 +8,6 @@ import (
"codeberg.org/gruf/go-kv/v2/format" "codeberg.org/gruf/go-kv/v2/format"
) )
var formatter format.Formatter
var argsDefault = format.DefaultArgs() var argsDefault = format.DefaultArgs()
var argsVerbose = func() format.Args { var argsVerbose = func() format.Args {
@ -29,7 +27,7 @@ func (f Field) AppendFormat(buf *byteutil.Buffer, vbose bool) {
} }
AppendQuoteString(buf, f.K) AppendQuoteString(buf, f.K)
buf.WriteByte('=') 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. // Value returns the formatted value string of this Field.
@ -41,6 +39,6 @@ func (f Field) Value(vbose bool) string {
args = argsDefault args = argsDefault
} }
buf := make([]byte, 0, bufsize/2) 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) return byteutil.B2S(buf)
} }

View file

@ -1,4 +1,4 @@
//go:build go1.24 && !go1.25 //go:build go1.24 && !go1.26
package format package format
@ -39,26 +39,36 @@ type abi_EmptyInterface struct {
Data unsafe.Pointer 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() // see: go/src/internal/abi/type.go Type.Kind()
func abi_Type_Kind(t reflect.Type) uint8 { func abi_Type_Kind(t reflect.Type) uint8 {
iface := (*reflect_nonEmptyInterface)(unsafe.Pointer(&t)) iface := (*abi_NonEmptyInterface)(unsafe.Pointer(&t))
atype := (*abi_Type)(unsafe.Pointer(iface.word)) atype := (*abi_Type)(unsafe.Pointer(iface.Data))
return atype.Kind_ & abi_KindMask return atype.Kind_ & abi_KindMask
} }
// see: go/src/internal/abi/type.go Type.IfaceIndir() // see: go/src/internal/abi/type.go Type.IfaceIndir()
func abi_Type_IfaceIndir(t reflect.Type) bool { func abi_Type_IfaceIndir(t reflect.Type) bool {
iface := (*reflect_nonEmptyInterface)(unsafe.Pointer(&t)) iface := (*abi_NonEmptyInterface)(unsafe.Pointer(&t))
atype := (*abi_Type)(unsafe.Pointer(iface.word)) atype := (*abi_Type)(unsafe.Pointer(iface.Data))
return atype.Kind_&abi_KindDirectIface == 0 return atype.Kind_&abi_KindDirectIface == 0
} }
// pack_iface packs a new reflect.nonEmptyInterface{} using shielded itab // pack_iface packs a new reflect.nonEmptyInterface{} using shielded
// pointer and data (word) pointer, returning a pointer for caller casting. // itab and data pointer, returning a pointer for caller casting.
func pack_iface(itab uintptr, word unsafe.Pointer) unsafe.Pointer { func pack_iface(itab uintptr, word unsafe.Pointer) unsafe.Pointer {
return unsafe.Pointer(&reflect_nonEmptyInterface{ return unsafe.Pointer(&abi_NonEmptyInterface{
itab: itab, ITab: itab,
word: word, 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. // this is useful for later calls to pack_iface for known type.
func get_iface_ITab[I any](t reflect.Type) uintptr { func get_iface_ITab[I any](t reflect.Type) uintptr {
s := reflect.New(t).Elem().Interface().(I) s := reflect.New(t).Elem().Interface().(I)
i := (*reflect_nonEmptyInterface)(unsafe.Pointer(&s)) i := (*abi_NonEmptyInterface)(unsafe.Pointer(&s))
return i.itab return i.ITab
} }
// unpack_eface returns the .Data portion of an abi.EmptyInterface{}. // 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)) 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{}. // reflect_Value is a copy of the memory layout of reflect.Value{}.
// //
// see: go/src/reflect/value.go // see: go/src/reflect/value.go
@ -190,7 +191,7 @@ func init() {
// as the reflect.nonEmptyInterface{}, which itself will be a pointer // as the reflect.nonEmptyInterface{}, which itself will be a pointer
// to the actual abi.Type{} that this reflect.Type{} is wrapping. // to the actual abi.Type{} that this reflect.Type{} is wrapping.
func reflect_type_data(t reflect.Type) unsafe.Pointer { 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. // build_reflect_value manually builds a reflect.Value{} by setting the internal field members.

View file

@ -8,6 +8,9 @@ import (
"unsafe" "unsafe"
) )
// Global formatter instance.
var Global Formatter
// FormatFunc defines a function capable of formatting // FormatFunc defines a function capable of formatting
// the value contained in State{}.P, based on args in // the value contained in State{}.P, based on args in
// State{}.A, storing the result in buffer State{}.B. // State{}.A, storing the result in buffer State{}.B.
@ -48,7 +51,7 @@ const ringsz = 16
// ptr_ring is a ring buffer of pointers, // ptr_ring is a ring buffer of pointers,
// purposely stored as uintptrs as all we // 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. // we don't want to hold-up the GC.
type ptr_ring struct { type ptr_ring struct {
p [ringsz]uintptr p [ringsz]uintptr
@ -296,38 +299,60 @@ func (fmt *Formatter) get(t typenode) (fn FormatFunc) {
func (fmt *Formatter) getInterfaceType(t typenode) FormatFunc { func (fmt *Formatter) getInterfaceType(t typenode) FormatFunc {
if t.rtype.NumMethod() == 0 { if t.rtype.NumMethod() == 0 {
return func(s *State) { return func(s *State) {
// Unpack empty interface.
eface := *(*any)(s.P) eface := *(*any)(s.P)
s.P = unpack_eface(eface) s.P = unpack_eface(eface)
// Get reflected type information.
rtype := reflect.TypeOf(eface) rtype := reflect.TypeOf(eface)
if rtype == nil { if rtype == nil {
appendNil(s) appendNil(s)
return return
} }
// Check for ptr recursion.
if s.ifaces.contains(s.P) { if s.ifaces.contains(s.P) {
getPointerType(t)(s) getPointerType(t)(s)
return return
} }
// Store value ptr.
s.ifaces.set(s.P) s.ifaces.set(s.P)
// Wrap in our typenode for before load.
flags := reflect_iface_elem_flags(rtype) flags := reflect_iface_elem_flags(rtype)
t := new_typenode(rtype, flags) t := new_typenode(rtype, flags)
// Load + pass to func.
fmt.loadOrStore(t)(s) fmt.loadOrStore(t)(s)
} }
} else { } else {
return func(s *State) { return func(s *State) {
// Unpack interface-with-method ptr.
iface := *(*interface{ M() })(s.P) iface := *(*interface{ M() })(s.P)
s.P = unpack_eface(iface) s.P = unpack_eface(iface)
// Get reflected type information.
rtype := reflect.TypeOf(iface) rtype := reflect.TypeOf(iface)
if rtype == nil { if rtype == nil {
appendNil(s) appendNil(s)
return return
} }
// Check for ptr recursion.
if s.ifaces.contains(s.P) { if s.ifaces.contains(s.P) {
getPointerType(t)(s) getPointerType(t)(s)
return return
} }
// Store value ptr.
s.ifaces.set(s.P) s.ifaces.set(s.P)
// Wrap in our typenode for before load.
flags := reflect_iface_elem_flags(rtype) flags := reflect_iface_elem_flags(rtype)
t := new_typenode(rtype, flags) t := new_typenode(rtype, flags)
// Load + pass to func.
fmt.loadOrStore(t)(s) fmt.loadOrStore(t)(s)
} }
} }
@ -360,14 +385,11 @@ func getIntType(t typenode) FormatFunc {
switch { switch {
case s.A.AsNumber(): case s.A.AsNumber():
// fallthrough // fallthrough
case s.A.AsQuotedText():
s.B = strconv.AppendQuoteRune(s.B, *(*rune)(s.P))
return
case s.A.AsQuotedASCII(): case s.A.AsQuotedASCII():
s.B = strconv.AppendQuoteRuneToASCII(s.B, *(*rune)(s.P)) s.B = strconv.AppendQuoteRuneToASCII(s.B, *(*rune)(s.P))
return return
case s.A.AsText(): case s.A.AsText() || s.A.AsQuotedText():
s.B = AppendEscapeRune(s.B, *(*rune)(s.P)) s.B = strconv.AppendQuoteRune(s.B, *(*rune)(s.P))
return return
} }
appendInt(s, int64(*(*int32)(s.P))) appendInt(s, int64(*(*int32)(s.P)))
@ -388,12 +410,9 @@ func getUintType(t typenode) FormatFunc {
switch { switch {
case s.A.AsNumber(): case s.A.AsNumber():
// fallthrough // 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)) s.B = AppendQuoteByte(s.B, *(*byte)(s.P))
return return
case s.A.AsText():
s.B = AppendEscapeByte(s.B, *(*byte)(s.P))
return
} }
appendUint(s, uint64(*(*uint8)(s.P))) appendUint(s, uint64(*(*uint8)(s.P)))
}) })
@ -468,10 +487,17 @@ func with_typestr_ptrs(t typenode, fn FormatFunc) FormatFunc {
if fn == nil { if fn == nil {
panic("nil func") panic("nil func")
} }
// Check for type wrapping.
if !t.needs_typestr() { if !t.needs_typestr() {
return fn return fn
} }
// Get type string with pointers.
typestr := t.typestr_with_ptrs() typestr := t.typestr_with_ptrs()
// Wrap format func to include
// type information when needed.
return func(s *State) { return func(s *State) {
if s.A.WithType() { if s.A.WithType() {
s.B = append(s.B, "("+typestr+")("...) 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) { func appendString(s *State, v string) {
switch { switch {
case s.A.Logfmt() || s.A.WithType(): case s.A.WithType():
if len(v) > SingleTermLine || !IsSafeASCII(v) { if len(v) > SingleTermLine || !IsSafeASCII(v) {
// Requires quoting AND escaping // Requires quoting AND escaping
s.B = strconv.AppendQuote(s.B, v) s.B = strconv.AppendQuote(s.B, v)
@ -494,8 +520,22 @@ func appendString(s *State, v string) {
s.B = append(s.B, '"') s.B = append(s.B, '"')
s.B = AppendEscape(s.B, v) s.B = AppendEscape(s.B, v)
s.B = append(s.B, '"') s.B = append(s.B, '"')
} else if s.A.WithType() || } else {
len(v) == 0 || ContainsSpaceOrTab(v) { // 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 // Contains space / empty, needs quotes
s.B = append(s.B, '"') s.B = append(s.B, '"')
s.B = append(s.B, v...) s.B = append(s.B, v...)
@ -515,75 +555,114 @@ func appendString(s *State, v string) {
func appendInt(s *State, v int64) { func appendInt(s *State, v int64) {
args := s.A.Int args := s.A.Int
// Set argument defaults.
if args == zeroArgs.Int { if args == zeroArgs.Int {
args = defaultArgs.Int args = defaultArgs.Int
} }
// Add any padding.
if args.Pad > 0 { if args.Pad > 0 {
const zeros = `00000000000000000000` const zeros = `00000000000000000000`
if args.Pad > len(zeros) { if args.Pad > len(zeros) {
panic("cannot pad > " + zeros) panic("cannot pad > " + zeros)
} }
if v == 0 { if v == 0 {
s.B = append(s.B, zeros[:args.Pad]...) s.B = append(s.B, zeros[:args.Pad]...)
return return
} }
// Get absolute.
abs := abs64(v) abs := abs64(v)
// Get number of required chars.
chars := int(v / int64(args.Base)) chars := int(v / int64(args.Base))
if v%int64(args.Base) != 0 { if v%int64(args.Base) != 0 {
chars++ chars++
} }
if abs != v { if abs != v {
// If this is a negative value,
// prepend minus ourselves and
// set value as the absolute.
s.B = append(s.B, '-') s.B = append(s.B, '-')
v = abs 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) s.B = strconv.AppendInt(s.B, v, args.Base)
} }
func appendUint(s *State, v uint64) { func appendUint(s *State, v uint64) {
args := s.A.Int args := s.A.Int
// Set argument defaults.
if args == zeroArgs.Int { if args == zeroArgs.Int {
args = defaultArgs.Int args = defaultArgs.Int
} }
// Add any padding.
if args.Pad > 0 { if args.Pad > 0 {
const zeros = `00000000000000000000` const zeros = `00000000000000000000`
if args.Pad > len(zeros) { if args.Pad > len(zeros) {
panic("cannot pad > " + zeros) panic("cannot pad > " + zeros)
} }
if v == 0 { if v == 0 {
s.B = append(s.B, zeros[:args.Pad]...) s.B = append(s.B, zeros[:args.Pad]...)
return return
} }
// Get number of required chars.
chars := int(v / uint64(args.Base)) chars := int(v / uint64(args.Base))
if v%uint64(args.Base) != 0 { if v%uint64(args.Base) != 0 {
chars++ 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) s.B = strconv.AppendUint(s.B, v, args.Base)
} }
func appendFloat(s *State, v float64, bits int) { func appendFloat(s *State, v float64, bits int) {
args := s.A.Float args := s.A.Float
// Set argument defaults.
if args == zeroArgs.Float { if args == zeroArgs.Float {
args = defaultArgs.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) { func appendComplex(s *State, r, i float64, bits int) {
args := s.A.Complex args := s.A.Complex
// Set argument defaults.
if args == zeroArgs.Complex { if args == zeroArgs.Complex {
args = defaultArgs.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 = 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') s.B = append(s.B, 'i')
} }

View file

@ -45,7 +45,7 @@ func getInterfaceStringerType(t typenode) FormatFunc {
case true: case true:
return with_typestr_ptrs(t, func(s *State) { return with_typestr_ptrs(t, func(s *State) {
s.P = *(*unsafe.Pointer)(s.P) 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) appendNil(s)
return return
} }
@ -54,7 +54,7 @@ func getInterfaceStringerType(t typenode) FormatFunc {
}) })
case false: case false:
return with_typestr_ptrs(t, func(s *State) { 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) appendNil(s)
return return
} }
@ -102,7 +102,7 @@ func getInterfaceErrorType(t typenode) FormatFunc {
case true: case true:
return with_typestr_ptrs(t, func(s *State) { return with_typestr_ptrs(t, func(s *State) {
s.P = *(*unsafe.Pointer)(s.P) 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) appendNil(s)
return return
} }
@ -111,7 +111,7 @@ func getInterfaceErrorType(t typenode) FormatFunc {
}) })
case false: case false:
return with_typestr_ptrs(t, func(s *State) { 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) appendNil(s)
return return
} }

View file

@ -17,8 +17,8 @@ func AppendQuoteString(buf *byteutil.Buffer, str string) {
return return
case len(str) == 1: case len(str) == 1:
// Append escaped single byte. // Append quoted escaped single byte.
buf.B = format.AppendEscapeByte(buf.B, str[0]) buf.B = format.AppendQuoteByte(buf.B, str[0])
return return
case len(str) > format.SingleTermLine || !format.IsSafeASCII(str): case len(str) > format.SingleTermLine || !format.IsSafeASCII(str):
@ -62,7 +62,7 @@ func AppendQuoteValue(buf *byteutil.Buffer, str string) {
return return
case len(str) == 1: case len(str) == 1:
// Append quoted single byte. // Append quoted escaped single byte.
buf.B = format.AppendQuoteByte(buf.B, str[0]) buf.B = format.AppendQuoteByte(buf.B, str[0])
return return

5
vendor/modules.txt vendored
View file

@ -232,6 +232,9 @@ codeberg.org/gruf/go-byteutil
codeberg.org/gruf/go-cache/v3 codeberg.org/gruf/go-cache/v3
codeberg.org/gruf/go-cache/v3/simple codeberg.org/gruf/go-cache/v3/simple
codeberg.org/gruf/go-cache/v3/ttl 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 # codeberg.org/gruf/go-debug v1.3.0
## explicit; go 1.16 ## explicit; go 1.16
codeberg.org/gruf/go-debug codeberg.org/gruf/go-debug
@ -255,7 +258,7 @@ codeberg.org/gruf/go-iotools
## explicit; go 1.20 ## explicit; go 1.20
codeberg.org/gruf/go-kv codeberg.org/gruf/go-kv
codeberg.org/gruf/go-kv/format 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 ## explicit; go 1.24
codeberg.org/gruf/go-kv/v2 codeberg.org/gruf/go-kv/v2
codeberg.org/gruf/go-kv/v2/format codeberg.org/gruf/go-kv/v2/format