mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-28 03:52:25 -05:00
[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:
parent
96c05a90a2
commit
7af9117e0d
37 changed files with 1070 additions and 439 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
7
go.mod
7
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
|
||||
|
|
|
|||
6
go.sum
generated
6
go.sum
generated
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
}...)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
}
|
||||
|
|
|
|||
90
internal/log/format/format.go
Normal file
90
internal/log/format/format.go
Normal 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...)
|
||||
}
|
||||
60
internal/log/format/format_test.go
Normal file
60
internal/log/format/format_test.go
Normal 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
241
internal/log/format/json.go
Normal 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,
|
||||
}
|
||||
67
internal/log/format/logfmt.go
Normal file
67
internal/log/format/logfmt.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
45
internal/log/level/level.go
Normal file
45
internal/log/level/level.go
Normal 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",
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ./...
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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: "",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
9
vendor/codeberg.org/gruf/go-caller/LICENSE
generated
vendored
Normal file
9
vendor/codeberg.org/gruf/go-caller/LICENSE
generated
vendored
Normal 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
3
vendor/codeberg.org/gruf/go-caller/README.md
generated
vendored
Normal 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
136
vendor/codeberg.org/gruf/go-caller/caller.go
generated
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
6
vendor/codeberg.org/gruf/go-kv/v2/field_format.go
generated
vendored
6
vendor/codeberg.org/gruf/go-kv/v2/field_format.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
45
vendor/codeberg.org/gruf/go-kv/v2/format/abi.go
generated
vendored
45
vendor/codeberg.org/gruf/go-kv/v2/format/abi.go
generated
vendored
|
|
@ -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.
|
||||
|
|
|
|||
123
vendor/codeberg.org/gruf/go-kv/v2/format/format.go
generated
vendored
123
vendor/codeberg.org/gruf/go-kv/v2/format/format.go
generated
vendored
|
|
@ -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')
|
||||
}
|
||||
|
||||
|
|
|
|||
8
vendor/codeberg.org/gruf/go-kv/v2/format/methods.go
generated
vendored
8
vendor/codeberg.org/gruf/go-kv/v2/format/methods.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
6
vendor/codeberg.org/gruf/go-kv/v2/util.go
generated
vendored
6
vendor/codeberg.org/gruf/go-kv/v2/util.go
generated
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
5
vendor/modules.txt
vendored
5
vendor/modules.txt
vendored
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue