initial commit
This commit is contained in:
commit
cacb699718
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.direnv
|
||||
|
8
cli/cli.go
Normal file
8
cli/cli.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package cli
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "redmage",
|
||||
Short: "Redmage is an HTTP server to download images from Reddit.",
|
||||
}
|
16
cli/serve.go
Normal file
16
cli/serve.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package cli
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Starts the HTTP Server",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(serveCmd)
|
||||
}
|
44
config/config.go
Normal file
44
config/config.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/knadh/koanf/providers/confmap"
|
||||
"github.com/knadh/koanf/v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
*koanf.Koanf
|
||||
}
|
||||
|
||||
func EmptyConfig() *Config {
|
||||
return NewConfigBuilder().Build()
|
||||
}
|
||||
|
||||
type ConfigBuilder struct {
|
||||
koanf *koanf.Koanf
|
||||
err error
|
||||
}
|
||||
|
||||
func NewConfigBuilder() *ConfigBuilder {
|
||||
return &ConfigBuilder{koanf: koanf.New(".")}
|
||||
}
|
||||
|
||||
func (builder *ConfigBuilder) Build() *Config {
|
||||
return &Config{Koanf: builder.koanf}
|
||||
}
|
||||
|
||||
func (builder *ConfigBuilder) BuildHandle() (*Config, error) {
|
||||
return &Config{Koanf: builder.koanf}, builder.err
|
||||
}
|
||||
|
||||
func (builder *ConfigBuilder) LoadDefault() *ConfigBuilder {
|
||||
provider := confmap.Provider(map[string]any{
|
||||
"log.enable": true,
|
||||
"log.source": true,
|
||||
"log.format": "pretty",
|
||||
"log.level": "info",
|
||||
"log.output": "stderr",
|
||||
}, ".")
|
||||
|
||||
_ = builder.koanf.Load(provider, nil)
|
||||
return builder
|
||||
}
|
127
errs/errs.go
Normal file
127
errs/errs.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package errs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Error interface {
|
||||
error
|
||||
Message(msg string, args ...any) Error
|
||||
GetMessage() string
|
||||
Code(status int) Error
|
||||
GetCode() int
|
||||
Caller(pc uintptr) Error
|
||||
GetCaller() uintptr
|
||||
Details(...any) Error
|
||||
GetDetails() []any
|
||||
Log(ctx context.Context) Error
|
||||
}
|
||||
|
||||
var _ Error = (*Err)(nil)
|
||||
|
||||
type Err struct {
|
||||
msg string
|
||||
code int
|
||||
caller uintptr
|
||||
details []any
|
||||
origin error
|
||||
}
|
||||
|
||||
func (er *Err) LogValue() slog.Value {
|
||||
values := make([]slog.Attr, 0, 5)
|
||||
|
||||
if er.msg != "" {
|
||||
values = append(values, slog.String("message", er.msg))
|
||||
}
|
||||
|
||||
if er.code != 0 {
|
||||
values = append(values, slog.Int("code", er.code))
|
||||
}
|
||||
if er.caller != 0 {
|
||||
frame, _ := runtime.CallersFrames([]uintptr{er.caller}).Next()
|
||||
split := strings.Split(frame.Function, string(os.PathSeparator))
|
||||
fnName := split[len(split)-1]
|
||||
|
||||
values = append(values, slog.Group("origin",
|
||||
slog.String("file", frame.File),
|
||||
slog.Int("line", frame.Line),
|
||||
slog.String("function", fnName),
|
||||
))
|
||||
}
|
||||
|
||||
if len(er.details) > 0 {
|
||||
values = append(values, slog.Group("details", er.details...))
|
||||
}
|
||||
|
||||
values = append(values, slog.Group("error",
|
||||
slog.String("type", reflect.TypeOf(er.origin).String()),
|
||||
slog.Any("data", er.origin),
|
||||
))
|
||||
|
||||
return slog.GroupValue(values...)
|
||||
}
|
||||
|
||||
func (er *Err) Error() string {
|
||||
var (
|
||||
s = strings.Builder{}
|
||||
source = er.origin
|
||||
msg = er.msg + ": " + source.Error()
|
||||
)
|
||||
for unwrap := errors.Unwrap(source); unwrap != nil; source = unwrap {
|
||||
originMsg := unwrap.Error()
|
||||
// TODO: Test this!
|
||||
if cut, found := strings.CutSuffix(msg, originMsg); found {
|
||||
s.WriteString(cut)
|
||||
msg = originMsg
|
||||
} else {
|
||||
s.WriteString(msg)
|
||||
}
|
||||
|
||||
s.WriteString(": ")
|
||||
|
||||
unwrap = errors.Unwrap(unwrap)
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (er *Err) Message(msg string, args ...any) Error {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (er *Err) GetMessage() string {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (er *Err) Code(status int) Error {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (er *Err) GetCode() int {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (er *Err) Caller(pc uintptr) Error {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (er *Err) GetCaller() uintptr {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (er *Err) Details(_ ...any) Error {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (er *Err) GetDetails() []any {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (er *Err) Log(ctx context.Context) Error {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
122
flake.lock
Normal file
122
flake.lock
Normal file
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"templ",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694102001,
|
||||
"narHash": "sha256-vky6VPK1n1od6vXbqzOXnekrQpTL4hbPAwUhT5J9c9E=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "9e21c80adf67ebcb077d75bd5e7d724d21eeafd6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1712192574,
|
||||
"narHash": "sha256-LbbVOliJKTF4Zl2b9salumvdMXuQBr2kuKP5+ZwbYq4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f480f9d09e4b4cf87ee6151eba068197125714de",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1694422566,
|
||||
"narHash": "sha256-lHJ+A9esOz9vln/3CJG23FV6Wd2OoOFbDeEs4cMGMqc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3a2786eea085f040a66ecde1bc3ddc7099f6dbeb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"templ": "templ"
|
||||
}
|
||||
},
|
||||
"templ": {
|
||||
"inputs": {
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"xc": "xc"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1706214512,
|
||||
"narHash": "sha256-Z2WyXMmOFk72U94f35RMzx41LDtji3+8HOfLHcbaJyI=",
|
||||
"owner": "a-h",
|
||||
"repo": "templ",
|
||||
"rev": "1f30f822a6edfdbfbab9e6851b1ff61e0ab01d4f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "a-h",
|
||||
"ref": "v0.2.542",
|
||||
"repo": "templ",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"xc": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"templ",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703164129,
|
||||
"narHash": "sha256-kCcCqqwvjN07H8FPG4tXsRVRcMqT8dUNt9pwW1kKAe8=",
|
||||
"owner": "joerdav",
|
||||
"repo": "xc",
|
||||
"rev": "0655cccfcf036556aeaddfb8f45dc7e8dd1b3680",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "joerdav",
|
||||
"repo": "xc",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
24
flake.nix
Normal file
24
flake.nix
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
inputs = {
|
||||
templ.url = "github:a-h/templ/v0.2.542"; # 0.2.542
|
||||
nixpkgs.url = "nixpkgs/nixpkgs-unstable";
|
||||
};
|
||||
|
||||
outputs = inputs@{ templ, nixpkgs, ... }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
templPkg = templ.packages.${system}.templ;
|
||||
pkgs = inputs.nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShell.${system} = pkgs.mkShell rec {
|
||||
name = "redmage-shell";
|
||||
buildInputs = with pkgs; [
|
||||
templPkg
|
||||
go
|
||||
modd
|
||||
nodejs_21
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
23
go.mod
Normal file
23
go.mod
Normal file
|
@ -0,0 +1,23 @@
|
|||
module github.com/tigorlazuardi/redmage
|
||||
|
||||
go 1.22.1
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.16.0
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/knadh/koanf/providers/confmap v0.1.0
|
||||
github.com/knadh/koanf/v2 v2.1.1
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/spf13/cobra v1.8.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
)
|
35
go.sum
Normal file
35
go.sum
Normal file
|
@ -0,0 +1,35 @@
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
|
||||
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU=
|
||||
github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU=
|
||||
github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
|
||||
github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
21
log/context.go
Normal file
21
log/context.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type loggerKey struct{}
|
||||
|
||||
func FromContext(ctx context.Context) slog.Handler {
|
||||
h, _ := ctx.Value(loggerKey{}).(slog.Handler)
|
||||
return h
|
||||
}
|
||||
|
||||
func WithContext(ctx context.Context, l slog.Handler) context.Context {
|
||||
return context.WithValue(ctx, loggerKey{}, l)
|
||||
}
|
||||
|
||||
func NullHandlerContext(ctx context.Context) context.Context {
|
||||
return WithContext(ctx, NullHandler{})
|
||||
}
|
151
log/log.go
Normal file
151
log/log.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/tigorlazuardi/redmage/config"
|
||||
)
|
||||
|
||||
var handler slog.Handler = NullHandler{}
|
||||
|
||||
func NewHandler(cfg *config.Config) slog.Handler {
|
||||
if !cfg.Bool("log.enable") {
|
||||
return NullHandler{}
|
||||
}
|
||||
var output io.Writer
|
||||
if strings.ToLower(cfg.String("log.output")) == "stdout" {
|
||||
output = colorable.NewColorableStdout()
|
||||
} else {
|
||||
output = colorable.NewColorableStderr()
|
||||
}
|
||||
|
||||
var lvl slog.Level
|
||||
_ = lvl.UnmarshalText(cfg.Bytes("log.level"))
|
||||
opts := &slog.HandlerOptions{
|
||||
AddSource: cfg.Bool("log.source"),
|
||||
Level: lvl,
|
||||
}
|
||||
|
||||
format := strings.ToLower(cfg.String("log.format"))
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) && format == "pretty" {
|
||||
return NewPrettyHandler(output, opts)
|
||||
} else {
|
||||
return slog.NewJSONHandler(output, opts)
|
||||
}
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
ctx context.Context
|
||||
handler slog.Handler
|
||||
caller uintptr
|
||||
time time.Time
|
||||
}
|
||||
|
||||
// Log prepares a new entry to write logs.
|
||||
func Log(ctx context.Context) *Entry {
|
||||
h := FromContext(ctx)
|
||||
if h == nil {
|
||||
h = handler
|
||||
}
|
||||
return &Entry{ctx: ctx, handler: h, time: time.Now()}
|
||||
}
|
||||
|
||||
func (entry *Entry) Caller(pc uintptr) *Entry {
|
||||
entry.caller = pc
|
||||
return entry
|
||||
}
|
||||
|
||||
func (entry *Entry) Info(message string, fields ...any) {
|
||||
record := slog.NewRecord(entry.time, slog.LevelInfo, message, entry.getCaller())
|
||||
record.AddAttrs(entry.getExtra()...)
|
||||
record.AddAttrs(slog.Group("context", fields...))
|
||||
_ = entry.handler.Handle(entry.ctx, record)
|
||||
}
|
||||
|
||||
func (entry *Entry) Infof(format string, args ...any) {
|
||||
message := fmt.Sprintf(format, args...)
|
||||
record := slog.NewRecord(entry.time, slog.LevelInfo, message, entry.getCaller())
|
||||
record.AddAttrs(entry.getExtra()...)
|
||||
_ = entry.handler.Handle(entry.ctx, record)
|
||||
}
|
||||
|
||||
func (entry *Entry) Error(message string, fields ...any) {
|
||||
record := slog.NewRecord(entry.time, slog.LevelError, message, entry.getCaller())
|
||||
record.AddAttrs(entry.getExtra()...)
|
||||
record.AddAttrs(slog.Group("context", fields...))
|
||||
_ = entry.handler.Handle(entry.ctx, record)
|
||||
}
|
||||
|
||||
func (entry *Entry) Errorf(format string, args ...any) {
|
||||
message := fmt.Sprintf(format, args...)
|
||||
record := slog.NewRecord(entry.time, slog.LevelError, message, entry.getCaller())
|
||||
record.AddAttrs(entry.getExtra()...)
|
||||
_ = entry.handler.Handle(entry.ctx, record)
|
||||
}
|
||||
|
||||
func (entry *Entry) Debug(message string, fields ...any) {
|
||||
record := slog.NewRecord(entry.time, slog.LevelDebug, message, entry.getCaller())
|
||||
record.AddAttrs(entry.getExtra()...)
|
||||
record.AddAttrs(slog.Group("context", fields...))
|
||||
_ = entry.handler.Handle(entry.ctx, record)
|
||||
}
|
||||
|
||||
func (entry *Entry) Debugf(format string, args ...any) {
|
||||
message := fmt.Sprintf(format, args...)
|
||||
record := slog.NewRecord(entry.time, slog.LevelDebug, message, entry.getCaller())
|
||||
record.AddAttrs(entry.getExtra()...)
|
||||
_ = entry.handler.Handle(entry.ctx, record)
|
||||
}
|
||||
|
||||
func (entry *Entry) Warn(message string, fields ...any) {
|
||||
record := slog.NewRecord(entry.time, slog.LevelWarn, message, entry.getCaller())
|
||||
record.AddAttrs(entry.getExtra()...)
|
||||
record.AddAttrs(slog.Group("context", fields...))
|
||||
_ = entry.handler.Handle(entry.ctx, record)
|
||||
}
|
||||
|
||||
func (entry *Entry) Warnf(format string, args ...any) {
|
||||
message := fmt.Sprintf(format, args...)
|
||||
record := slog.NewRecord(entry.time, slog.LevelWarn, message, entry.getCaller())
|
||||
record.AddAttrs(entry.getExtra()...)
|
||||
_ = entry.handler.Handle(entry.ctx, record)
|
||||
}
|
||||
|
||||
func (entry *Entry) getCaller() uintptr {
|
||||
if entry.caller != 0 {
|
||||
return entry.caller
|
||||
}
|
||||
return GetCaller(4)
|
||||
}
|
||||
|
||||
func GetCaller(skip int) uintptr {
|
||||
pc := make([]uintptr, 1)
|
||||
n := runtime.Callers(skip, pc)
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
return pc[0]
|
||||
}
|
||||
|
||||
func (entry *Entry) getExtra() []slog.Attr {
|
||||
out := make([]slog.Attr, 0, 1)
|
||||
if reqid := middleware.GetReqID(entry.ctx); reqid != "" {
|
||||
out = append(out, slog.String("request.id", reqid))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func SetDefault(h slog.Handler) {
|
||||
handler = h
|
||||
}
|
13
log/nil_handler.go
Normal file
13
log/nil_handler.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type NullHandler struct{}
|
||||
|
||||
func (NullHandler) Enabled(context.Context, slog.Level) bool { return false }
|
||||
func (NullHandler) Handle(context.Context, slog.Record) error { return nil }
|
||||
func (nu NullHandler) WithAttrs([]slog.Attr) slog.Handler { return nu }
|
||||
func (nu NullHandler) WithGroup(string) slog.Handler { return nu }
|
179
log/pretty_handler.go
Normal file
179
log/pretty_handler.go
Normal file
|
@ -0,0 +1,179 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
type PrettyHandler struct {
|
||||
mu sync.Mutex
|
||||
opts *slog.HandlerOptions
|
||||
output io.Writer
|
||||
replaceAttr func(groups []string, attr slog.Attr) slog.Attr
|
||||
withAttrs []slog.Attr
|
||||
withGroup []string
|
||||
}
|
||||
|
||||
// NewPrettyHandler creates a human friendly readable logs.
|
||||
func NewPrettyHandler(writer io.Writer, opts *slog.HandlerOptions) *PrettyHandler {
|
||||
if opts == nil {
|
||||
opts = &slog.HandlerOptions{}
|
||||
}
|
||||
if opts.ReplaceAttr == nil {
|
||||
opts.ReplaceAttr = func(groups []string, attr slog.Attr) slog.Attr { return attr }
|
||||
}
|
||||
return &PrettyHandler{
|
||||
opts: opts,
|
||||
output: writer,
|
||||
replaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
|
||||
if len(groups) > 0 {
|
||||
return opts.ReplaceAttr(groups, attr)
|
||||
}
|
||||
switch attr.Key {
|
||||
case slog.TimeKey, slog.LevelKey, slog.SourceKey, slog.MessageKey:
|
||||
return slog.Attr{}
|
||||
default:
|
||||
return opts.ReplaceAttr(groups, attr)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled implements slog.Handler interface.
|
||||
func (pr *PrettyHandler) Enabled(ctx context.Context, lvl slog.Level) bool {
|
||||
return pr.opts.Level.Level() <= lvl
|
||||
}
|
||||
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
buf := &bytes.Buffer{}
|
||||
buf.Grow(1024)
|
||||
return buf
|
||||
},
|
||||
}
|
||||
|
||||
func putBuffer(buf *bytes.Buffer) {
|
||||
const limit = 1024 * 512 // 512KB
|
||||
if buf.Cap() < limit {
|
||||
buf.Reset()
|
||||
bufferPool.Put(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle implements slog.Handler interface.
|
||||
func (pr *PrettyHandler) Handle(ctx context.Context, record slog.Record) error {
|
||||
var levelColor *color.Color
|
||||
switch {
|
||||
case record.Level >= slog.LevelError:
|
||||
levelColor = color.New(color.FgRed)
|
||||
case record.Level >= slog.LevelWarn:
|
||||
levelColor = color.New(color.FgYellow)
|
||||
case record.Level >= slog.LevelInfo:
|
||||
levelColor = color.New(color.FgGreen)
|
||||
default:
|
||||
levelColor = color.New(color.FgWhite)
|
||||
}
|
||||
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
jsonBuf := bufferPool.Get().(*bytes.Buffer)
|
||||
defer putBuffer(buf)
|
||||
defer putBuffer(jsonBuf)
|
||||
|
||||
if record.PC != 0 && pr.opts.AddSource {
|
||||
frame := getFrame(record.PC)
|
||||
levelColor.Fprint(buf, frame.File)
|
||||
levelColor.Fprint(buf, ":")
|
||||
levelColor.Fprint(buf, frame.Line)
|
||||
levelColor.Fprint(buf, " -- ")
|
||||
split := strings.Split(frame.Function, string(os.PathSeparator))
|
||||
fnName := split[len(split)-1]
|
||||
levelColor.Fprint(buf, fnName)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
if !record.Time.IsZero() {
|
||||
buf.WriteString(record.Time.Format("[2006-01-02 15:04:05] "))
|
||||
}
|
||||
|
||||
buf.WriteByte('[')
|
||||
levelColor.Add(color.Bold).Fprint(buf, record.Level.String())
|
||||
buf.WriteString("] ")
|
||||
|
||||
if record.Message != "" {
|
||||
buf.WriteString(record.Message)
|
||||
}
|
||||
|
||||
buf.WriteByte('\n')
|
||||
|
||||
serializer := pr.createSerializer(jsonBuf)
|
||||
_ = serializer.Handle(ctx, record)
|
||||
if jsonBuf.Len() > 3 { // Ignore empty json like "{}\n"
|
||||
_ = json.Indent(buf, jsonBuf.Bytes(), "", " ")
|
||||
// json indent includes new line, no need to add extra text.
|
||||
} else {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
pr.mu.Lock()
|
||||
defer pr.mu.Unlock()
|
||||
_, err := buf.WriteTo(pr.output)
|
||||
return err
|
||||
}
|
||||
|
||||
func (pr *PrettyHandler) createSerializer(w io.Writer) slog.Handler {
|
||||
var jsonHandler slog.Handler = slog.NewJSONHandler(w, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
ReplaceAttr: pr.replaceAttr,
|
||||
})
|
||||
|
||||
if len(pr.withAttrs) > 0 {
|
||||
jsonHandler = jsonHandler.WithAttrs(pr.withAttrs)
|
||||
}
|
||||
|
||||
if len(pr.withGroup) > 0 {
|
||||
for _, group := range pr.withGroup {
|
||||
jsonHandler = jsonHandler.WithGroup(group)
|
||||
}
|
||||
}
|
||||
|
||||
return jsonHandler
|
||||
}
|
||||
|
||||
func getFrame(pc uintptr) runtime.Frame {
|
||||
frames := runtime.CallersFrames([]uintptr{pc})
|
||||
frame, _ := frames.Next()
|
||||
return frame
|
||||
}
|
||||
|
||||
func (pr *PrettyHandler) clone() *PrettyHandler {
|
||||
return &PrettyHandler{
|
||||
opts: pr.opts,
|
||||
output: pr.output,
|
||||
replaceAttr: pr.replaceAttr,
|
||||
withAttrs: pr.withAttrs,
|
||||
withGroup: pr.withGroup,
|
||||
}
|
||||
}
|
||||
|
||||
// WithAttrs implements slog.Handler interface.
|
||||
func (pr *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
p := pr.clone()
|
||||
p.withAttrs = append(p.withAttrs, attrs...)
|
||||
return p
|
||||
}
|
||||
|
||||
// WithGroup implements slog.Handler interface.
|
||||
func (pr *PrettyHandler) WithGroup(name string) slog.Handler {
|
||||
p := pr.clone()
|
||||
p.withGroup = append(p.withGroup, name)
|
||||
return p
|
||||
}
|
Loading…
Reference in a new issue