initial commit

This commit is contained in:
Tigor Hutasuhut 2024-04-06 01:22:00 +07:00
commit cacb699718
15 changed files with 780 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.direnv

8
cli/cli.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

14
main.go Normal file
View file

@ -0,0 +1,14 @@
package main
import (
"context"
"os"
"github.com/tigorlazuardi/redmage/cli"
)
func main() {
if err := cli.RootCmd.ExecuteContext(context.Background()); err != nil {
os.Exit(1)
}
}