diff --git a/config/go.mod b/config/go.mod index d0c1f58..1082c75 100644 --- a/config/go.mod +++ b/config/go.mod @@ -5,6 +5,6 @@ go 1.20 replace go.osspkg.com/x/test => ./../test require ( - go.osspkg.com/x/test v0.3.0 + go.osspkg.com/x/test v0.5.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/console/go.mod b/console/go.mod index c0a9cb2..823d6cf 100644 --- a/console/go.mod +++ b/console/go.mod @@ -4,4 +4,4 @@ go 1.20 replace go.osspkg.com/x/errors => ../errors -require go.osspkg.com/x/errors v0.3.2 +require go.osspkg.com/x/errors v0.5.0 diff --git a/context/combine.go b/context/combine.go new file mode 100644 index 0000000..a3c2a04 --- /dev/null +++ b/context/combine.go @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package context + +import ( + cc "context" + "reflect" +) + +func Join(multi ...cc.Context) (cc.Context, cc.CancelFunc) { + ctx, cancel := cc.WithCancel(cc.Background()) + + go func() { + cases := make([]reflect.SelectCase, 0, len(multi)) + for _, vv := range multi { + cases = append(cases, reflect.SelectCase{ + Dir: reflect.SelectRecv, + Chan: reflect.ValueOf(vv.Done()), + }) + } + chosen, _, _ := reflect.Select(cases) + switch chosen { + default: + cancel() + } + }() + + return ctx, cancel +} diff --git a/context/combine_test.go b/context/combine_test.go new file mode 100644 index 0000000..54d6ddb --- /dev/null +++ b/context/combine_test.go @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package context + +import ( + "context" + "fmt" + "testing" + "time" +) + +func TestUnit_Join(t *testing.T) { + c, cancel := Join(context.Background(), context.Background()) + if c == nil { + t.Fatalf("contexts.Join returned nil") + } + + select { + case <-c.Done(): + t.Fatalf("<-c.Done() == it should block") + default: + } + + cancel() + <-time.After(time.Second) + + select { + case <-c.Done(): + default: + t.Fatalf("<-c.Done() it shouldn't block") + } + + if got, want := fmt.Sprint(c), "context.Background.WithCancel"; got != want { + t.Fatalf("contexts.Join() = %q want %q", got, want) + } +} diff --git a/context/context.go b/context/context.go new file mode 100644 index 0000000..10ba6cc --- /dev/null +++ b/context/context.go @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package context + +import cc "context" + +type ( + _ctx struct { + ctx cc.Context + cancel cc.CancelFunc + } + + Context interface { + Close() + Context() cc.Context + Done() <-chan struct{} + } +) + +func New() Context { + ctx, cancel := cc.WithCancel(cc.Background()) + return &_ctx{ + ctx: ctx, + cancel: cancel, + } +} + +func NewContext(c cc.Context) Context { + ctx, cancel := cc.WithCancel(c) + return &_ctx{ + ctx: ctx, + cancel: cancel, + } +} + +// Close context close method +func (v *_ctx) Close() { + v.cancel() +} + +// Context general context +func (v *_ctx) Context() cc.Context { + return v.ctx +} + +// Done context close wait channel +func (v *_ctx) Done() <-chan struct{} { + return v.ctx.Done() +} diff --git a/context/go.mod b/context/go.mod new file mode 100644 index 0000000..918c59c --- /dev/null +++ b/context/go.mod @@ -0,0 +1,3 @@ +module go.osspkg.com/x/context + +go 1.20 diff --git a/context/go.sum b/context/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/go.work b/go.work index 132e866..46ffbcc 100644 --- a/go.work +++ b/go.work @@ -5,11 +5,15 @@ use ( ./algorithms ./config ./console + ./context ./domain ./env ./errors + ./graphics ./io + ./log ./random + ./routine ./sync ./syscall ./test diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..0194871 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,4 @@ +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= diff --git a/graphics/go.mod b/graphics/go.mod new file mode 100644 index 0000000..fd45e4d --- /dev/null +++ b/graphics/go.mod @@ -0,0 +1,10 @@ +module go.osspkg.com/x/graphics + +go 1.20 + +replace go.osspkg.com/x/errors => ../errors + +require ( + go.osspkg.com/x/errors v0.5.0 + golang.org/x/image v0.18.0 +) diff --git a/graphics/go.sum b/graphics/go.sum new file mode 100644 index 0000000..9d0133a --- /dev/null +++ b/graphics/go.sum @@ -0,0 +1,2 @@ +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= diff --git a/graphics/images.go b/graphics/images.go new file mode 100644 index 0000000..862ebb4 --- /dev/null +++ b/graphics/images.go @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package graphics + +import ( + "crypto/sha1" + "fmt" + "image" + "image/jpeg" + "image/png" + "io" + "os" + "path/filepath" + "strings" + "sync" + + "go.osspkg.com/x/errors" + "golang.org/x/image/bmp" + "golang.org/x/image/draw" + "golang.org/x/image/tiff" + "golang.org/x/image/webp" +) + +var ( + ExtNotSupported = errors.New("ext is not supported") +) + +type ( + ImageScale struct { + folder string + cache map[string]ImageInfo + mux sync.Mutex + } + + ImageInfo struct { + Hash string + Origin string + Scale string + Thumb string + } +) + +var decoders = map[string]func(r io.Reader) (image.Image, error){ + ".jpg": jpeg.Decode, + ".jpeg": jpeg.Decode, + ".png": png.Decode, + ".webp": webp.Decode, + ".bmp": bmp.Decode, + ".tiff": tiff.Decode, +} + +func NewImageScale() *ImageScale { + return &ImageScale{ + folder: "", + cache: make(map[string]ImageInfo, 100), + } +} + +func (v *ImageScale) SetFolder(dir string) error { + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("create image folder: %w", err) + } + v.folder = dir + return nil +} + +func (v *ImageScale) Build(filename string, scale, thumb int) (*ImageInfo, error) { + var err error + img := &ImageInfo{} + + if img.Hash, err = v.getHash(filename); err != nil { + return nil, err + } + + v.mux.Lock() + i, ok := v.cache[img.Hash] + v.mux.Unlock() + if ok { + return &i, nil + } + + if img.Origin, err = v.resize(filename, img.Hash+".orig", 0); err != nil { + return nil, err + } + if img.Scale, err = v.resize(filename, img.Hash+".scale", scale); err != nil { + return nil, err + } + if img.Thumb, err = v.resize(filename, img.Hash+".thumb", thumb); err != nil { + return nil, err + } + + v.mux.Lock() + v.cache[img.Hash] = *img + v.mux.Unlock() + + return img, nil +} + +func (v *ImageScale) resize(filename, suffix string, width int) (string, error) { + src, name, err := v.readFile(filename) + if err != nil { + return "", err + } + x, y := v.scaleFactor(src.Bounds().Max.X, src.Bounds().Max.Y, width) + dst := image.NewNRGBA(image.Rect(0, 0, x, y)) + draw.CatmullRom.Scale(dst, dst.Rect, src, src.Bounds(), draw.Over, nil) + newFilename := fmt.Sprintf("%s-%s.png", name, suffix) + return newFilename, v.writeFile(v.folder+"/"+newFilename, dst) +} + +func (v *ImageScale) scaleFactor(oW, oH, width int) (int, int) { + if width == 0 { + return oW, oH + } + oWF, oHF := float64(oW), float64(oH) + nWidth := float64(width) + scale := oWF / nWidth + return int(oWF / scale), int(oHF / scale) +} + +func (v *ImageScale) writeFile(filename string, img image.Image) error { + file, err := os.Create(filename) + if err != nil { + return fmt.Errorf("write image `%s`: %w", filename, err) + } + if err = png.Encode(file, img); err != nil { + return errors.Wrap( + fmt.Errorf("encode image `%s`: %w", filename, err), + file.Close(), + ) + } + return file.Close() +} + +func (v *ImageScale) readFile(filename string) (image.Image, string, error) { + ext := filepath.Ext(filename) + dec, ok := decoders[ext] + if !ok { + return nil, "", ExtNotSupported + } + file, err := os.Open(filename) + if err != nil { + return nil, "", fmt.Errorf("read image `%s`: %w", filename, err) + } + img, err := dec(file) + if err != nil { + return nil, "", errors.Wrap( + fmt.Errorf("decode image `%s`: %w", filename, err), + file.Close(), + ) + } + fi, err := file.Stat() + if err != nil { + return nil, "", errors.Wrap( + fmt.Errorf("info image `%s`: %w", filename, err), + file.Close(), + ) + } + if err = file.Close(); err != nil { + return nil, "", fmt.Errorf("close image `%s`: %w", filename, err) + } + return img, strings.Replace(fi.Name(), ext, "", 1), nil +} + +func (v *ImageScale) getHash(filename string) (string, error) { + file, err := os.Open(filename) + if err != nil { + return "", fmt.Errorf("read image `%s`: %w", filename, err) + } + h := sha1.New() + if _, err = io.Copy(h, file); err != nil { + return "", errors.Wrap( + fmt.Errorf("calc hash image `%s`: %w", filename, err), + file.Close(), + ) + } + if err = file.Close(); err != nil { + return "", fmt.Errorf("close image `%s`: %w", filename, err) + } + return fmt.Sprintf("%x", h.Sum(nil)), nil +} diff --git a/io/go.mod b/io/go.mod index b964728..ad6669a 100644 --- a/io/go.mod +++ b/io/go.mod @@ -9,8 +9,8 @@ replace ( ) require ( - go.osspkg.com/x/errors v0.3.1 - go.osspkg.com/x/sync v0.3.0 - go.osspkg.com/x/test v0.3.0 + go.osspkg.com/x/errors v0.5.0 + go.osspkg.com/x/sync v0.5.0 + go.osspkg.com/x/test v0.5.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/log/common.go b/log/common.go new file mode 100644 index 0000000..e656fae --- /dev/null +++ b/log/common.go @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package log + +import "io" + +const ( + levelFatal uint32 = iota + LevelError + LevelWarn + LevelInfo + LevelDebug +) + +var levels = map[uint32]string{ + levelFatal: "FAT", + LevelError: "ERR", + LevelWarn: "WRN", + LevelInfo: "INF", + LevelDebug: "DBG", +} + +type Fields map[string]interface{} + +type Sender interface { + PutEntity(v *entity) + SendMessage(level uint32, call func(v *Message)) + Close() +} + +// Writer interface +type Writer interface { + Fatalf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + Warnf(format string, args ...interface{}) + Infof(format string, args ...interface{}) + Debugf(format string, args ...interface{}) +} + +type WriterContext interface { + WithError(key string, err error) Writer + WithField(key string, value interface{}) Writer + WithFields(Fields) Writer + Writer +} + +// Logger base interface +type Logger interface { + SetOutput(out io.Writer) + SetFormatter(f Formatter) + SetLevel(v uint32) + GetLevel() uint32 + Close() + + WriterContext +} diff --git a/log/default.go b/log/default.go new file mode 100644 index 0000000..74c4914 --- /dev/null +++ b/log/default.go @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package log + +import "io" + +var std = New() + +// Default logger +func Default() Logger { + return std +} + +// SetOutput change writer +func SetOutput(out io.Writer) { + std.SetOutput(out) +} + +func SetFormatter(f Formatter) { + std.SetFormatter(f) +} + +// SetLevel change log level +func SetLevel(v uint32) { + std.SetLevel(v) +} + +// GetLevel getting log level +func GetLevel() uint32 { + return std.GetLevel() +} + +// Close waiting for all messages to finish recording +func Close() { + std.Close() +} + +// Infof info message +func Infof(format string, args ...interface{}) { + std.Infof(format, args...) +} + +// Warnf warning message +func Warnf(format string, args ...interface{}) { + std.Warnf(format, args...) +} + +// Errorf error message +func Errorf(format string, args ...interface{}) { + std.Errorf(format, args...) +} + +// Debugf debug message +func Debugf(format string, args ...interface{}) { + std.Debugf(format, args...) +} + +// Fatalf fatal message and exit +func Fatalf(format string, args ...interface{}) { + std.Fatalf(format, args...) +} + +// WithFields setter context to log message +func WithFields(v Fields) Writer { + return std.WithFields(v) +} + +// WithError setter context to log message +func WithError(key string, err error) Writer { + return std.WithError(key, err) +} + +// WithField setter context to log message +func WithField(key string, value interface{}) Writer { + return std.WithField(key, value) +} diff --git a/log/entity.go b/log/entity.go new file mode 100644 index 0000000..d054d49 --- /dev/null +++ b/log/entity.go @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package log + +import ( + "fmt" + "os" + "reflect" +) + +type entity struct { + log Sender + ctx Fields +} + +func newEntity(log Sender) *entity { + return &entity{ + log: log, + ctx: Fields{}, + } +} + +func (e *entity) Reset() { + e.ctx = Fields{} +} + +func (e *entity) WithError(key string, err error) Writer { + if err != nil { + e.ctx[key] = err.Error() + } else { + e.ctx[key] = nil + } + return e +} + +func (e *entity) WithField(key string, value interface{}) Writer { + ref := reflect.TypeOf(value) + if ref != nil { + switch ref.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Struct: + e.ctx[key] = fmt.Sprintf("unsupported field value: %#v", value) + return e + } + } + e.ctx[key] = value + return e +} + +func (e *entity) WithFields(fields Fields) Writer { + for key, value := range fields { + ref := reflect.TypeOf(value) + if ref != nil { + switch ref.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Struct: + e.ctx[key] = fmt.Sprintf("unsupported field value: %#v", value) + continue + } + } + e.ctx[key] = value + } + return e +} + +func (e *entity) prepareMessage(format string, args ...interface{}) func(v *Message) { + return func(v *Message) { + v.Message = fmt.Sprintf(format, args...) + for key, value := range e.ctx { + v.Ctx[key] = value + } + e.log.PutEntity(e) + } +} + +// Infof info message +func (e *entity) Infof(format string, args ...interface{}) { + e.log.SendMessage(LevelInfo, e.prepareMessage(format, args...)) +} + +// Warnf warning message +func (e *entity) Warnf(format string, args ...interface{}) { + e.log.SendMessage(LevelWarn, e.prepareMessage(format, args...)) +} + +// Errorf error message +func (e *entity) Errorf(format string, args ...interface{}) { + e.log.SendMessage(LevelError, e.prepareMessage(format, args...)) +} + +// Debugf debug message +func (e *entity) Debugf(format string, args ...interface{}) { + e.log.SendMessage(LevelDebug, e.prepareMessage(format, args...)) +} + +// Fatalf fatal message and exit +func (e *entity) Fatalf(format string, args ...interface{}) { + e.log.SendMessage(levelFatal, e.prepareMessage(format, args...)) + e.log.Close() + os.Exit(1) +} diff --git a/log/formatter.go b/log/formatter.go new file mode 100644 index 0000000..ee033e9 --- /dev/null +++ b/log/formatter.go @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package log + +import ( + "bytes" + "encoding/json" + "fmt" + "sync" + "time" +) + +type Formatter interface { + Encode(m *Message) ([]byte, error) +} + +type FormatJSON struct{} + +func NewFormatJSON() *FormatJSON { + return &FormatJSON{} +} + +func (*FormatJSON) Encode(m *Message) ([]byte, error) { + b, err := json.Marshal(m) + if err != nil { + return nil, err + } + return append(b, '\n'), nil +} + +var poolBuff = sync.Pool{ + New: func() interface{} { + return newBuff() + }, +} + +func newBuff() *bytes.Buffer { + return bytes.NewBuffer(nil) +} + +type FormatString struct { + delim string +} + +func NewFormatString() *FormatString { + return &FormatString{delim: "\t"} +} + +func (v *FormatString) SetDelimiter(d string) { + v.delim = d +} + +func (v *FormatString) Encode(m *Message) ([]byte, error) { + b, ok := poolBuff.Get().(*bytes.Buffer) + if !ok { + b = newBuff() + } + + defer func() { + b.Reset() + poolBuff.Put(b) + }() + + fmt.Fprintf(b, "time=%s%slvl=%s%smsg=%#v", time.Unix(m.UnixTime, 0).Format(time.RFC3339), v.delim, m.Level, v.delim, m.Message) + if len(m.Ctx) > 0 { + for key, value := range m.Ctx { + fmt.Fprintf(b, "%s%s=%#v", v.delim, key, value) + } + } + b.WriteString("\n") + + return append(make([]byte, 0, b.Len()), b.Bytes()...), nil +} diff --git a/log/formatter_test.go b/log/formatter_test.go new file mode 100644 index 0000000..f212b35 --- /dev/null +++ b/log/formatter_test.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package log + +import ( + "bytes" + "testing" +) + +func TestUnit_FormatString_Encode(t *testing.T) { + tests := []struct { + name string + args *Message + want []byte + wantErr bool + }{ + { + name: "Case1", + args: &Message{ + UnixTime: 123456789, + Level: "INF", + Message: "Hello", + Ctx: map[string]interface{}{ + "err": "err\nmsg", + }, + }, + want: []byte("lvl=INF\tmsg=\"Hello\"\terr=\"err\\nmsg\"\n"), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fo := NewFormatString() + got, err := fo.Encode(tt.args) + if (err != nil) != tt.wantErr { + t.Errorf("Encode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !bytes.Contains(got, tt.want) { + t.Errorf("Encode() got = %v, want %v", string(got), string(tt.want)) + } + }) + } +} diff --git a/log/go.mod b/log/go.mod new file mode 100644 index 0000000..467bb54 --- /dev/null +++ b/log/go.mod @@ -0,0 +1,16 @@ +module go.osspkg.com/x/log + +go 1.20 + +replace ( + go.osspkg.com/x/sync => ../sync + go.osspkg.com/x/test => ../test +) + +require ( + github.com/mailru/easyjson v0.7.7 + go.osspkg.com/x/sync v0.5.0 + go.osspkg.com/x/test v0.5.0 +) + +require github.com/josharian/intern v1.0.0 // indirect diff --git a/log/go.sum b/log/go.sum new file mode 100644 index 0000000..7707cb6 --- /dev/null +++ b/log/go.sum @@ -0,0 +1,4 @@ +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= diff --git a/log/logger.go b/log/logger.go new file mode 100644 index 0000000..943c137 --- /dev/null +++ b/log/logger.go @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package log + +import ( + "io" + "os" + "sync" + "sync/atomic" + "time" + + xs "go.osspkg.com/x/sync" +) + +// log base model +type log struct { + status uint32 + writer io.Writer + entities sync.Pool + formatter Formatter + channel chan []byte + mux xs.Lock + wg xs.Group +} + +// New init new logger +func New() Logger { + object := &log{ + status: LevelError, + writer: os.Stdout, + formatter: NewFormatJSON(), + channel: make(chan []byte, 1024), + wg: xs.NewGroup(), + mux: xs.NewLock(), + } + object.entities = sync.Pool{ + New: func() interface{} { + return newEntity(object) + }, + } + object.wg.Background(func() { + object.queue() + }) + return object +} + +func (l *log) SendMessage(level uint32, call func(v *Message)) { + if l.GetLevel() < level { + return + } + + m, ok := poolMessage.Get().(*Message) + if !ok { + m = &Message{} + } + + call(m) + lvl, ok := levels[level] + if !ok { + lvl = "UNK" + } + m.Level, m.UnixTime = lvl, time.Now().Unix() + + l.mux.RLock(func() { + b, err := l.formatter.Encode(m) + if err != nil { + b = []byte(err.Error()) + } + + select { + case l.channel <- b: + default: + } + }) + + m.Reset() + poolMessage.Put(m) +} + +func (l *log) queue() { + for { + b, ok := <-l.channel + if !ok { + return + } + if b == nil { + return + } + l.mux.RLock(func() { + l.writer.Write(b) //nolint:errcheck + }) + } +} + +func (l *log) getEntity() *entity { + lw, ok := l.entities.Get().(*entity) + if !ok { + lw = newEntity(l) + } + return lw +} + +func (l *log) PutEntity(v *entity) { + v.Reset() + l.entities.Put(v) +} + +// Close waiting for all messages to finish recording +func (l *log) Close() { + l.channel <- nil + l.wg.Wait() +} + +// SetOutput change writer +func (l *log) SetOutput(out io.Writer) { + l.mux.Lock(func() { + l.writer = out + }) +} + +func (l *log) SetFormatter(f Formatter) { + l.mux.Lock(func() { + l.formatter = f + }) +} + +// SetLevel change log level +func (l *log) SetLevel(v uint32) { + atomic.StoreUint32(&l.status, v) +} + +// GetLevel getting log level +func (l *log) GetLevel() uint32 { + return atomic.LoadUint32(&l.status) +} + +// Infof info message +func (l *log) Infof(format string, args ...interface{}) { + l.getEntity().Infof(format, args...) +} + +// Warnf warning message +func (l *log) Warnf(format string, args ...interface{}) { + l.getEntity().Warnf(format, args...) +} + +// Errorf error message +func (l *log) Errorf(format string, args ...interface{}) { + l.getEntity().Errorf(format, args...) +} + +// Debugf debug message +func (l *log) Debugf(format string, args ...interface{}) { + l.getEntity().Debugf(format, args...) +} + +// Fatalf fatal message and exit +func (l *log) Fatalf(format string, args ...interface{}) { + l.getEntity().Fatalf(format, args...) +} + +// WithFields setter context to log message +func (l *log) WithFields(v Fields) Writer { + return l.getEntity().WithFields(v) +} + +// WithError setter context to log message +func (l *log) WithError(key string, err error) Writer { + return l.getEntity().WithError(key, err) +} + +// WithField setter context to log message +func (l *log) WithField(key string, value interface{}) Writer { + return l.getEntity().WithField(key, value) +} diff --git a/log/logger_test.go b/log/logger_test.go new file mode 100644 index 0000000..f355a04 --- /dev/null +++ b/log/logger_test.go @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package log + +import ( + "fmt" + "io" + "os" + "testing" + "time" + + "go.osspkg.com/x/sync" + "go.osspkg.com/x/test" +) + +func TestUnit_NewJSON(t *testing.T) { + test.NotNil(t, Default()) + + filename, err := os.CreateTemp(os.TempDir(), "test_new_default-*.log") + test.NoError(t, err) + + SetOutput(filename) + SetLevel(LevelDebug) + test.Equal(t, LevelDebug, GetLevel()) + + go Infof("async %d", 1) + go Warnf("async %d", 2) + go Errorf("async %d", 3) + go Debugf("async %d", 4) + + Infof("sync %d", 1) + Warnf("sync %d", 2) + Errorf("sync %d", 3) + Debugf("sync %d", 4) + + WithFields(Fields{"ip": "0.0.0.0"}).Infof("context1") + WithFields(Fields{"nil": nil}).Infof("context2") + WithFields(Fields{"func": func() {}}).Infof("context3") + + WithField("ip", "0.0.0.0").Infof("context4") + WithField("nil", nil).Infof("context5") + WithField("func", func() {}).Infof("context6") + + WithError("err", nil).Infof("context7") + WithError("err", fmt.Errorf("er1")).Infof("context8") + + <-time.After(time.Second * 1) + Close() + + test.NoError(t, filename.Close()) + data, err := os.ReadFile(filename.Name()) + test.NoError(t, err) + test.NoError(t, os.Remove(filename.Name())) + + sdata := string(data) + test.Contains(t, sdata, `"lvl":"INF","msg":"async 1"`) + test.Contains(t, sdata, `"lvl":"WRN","msg":"async 2"`) + test.Contains(t, sdata, `"lvl":"ERR","msg":"async 3"`) + test.Contains(t, sdata, `"lvl":"DBG","msg":"async 4"`) + test.Contains(t, sdata, `"lvl":"INF","msg":"sync 1"`) + test.Contains(t, sdata, `"lvl":"WRN","msg":"sync 2"`) + test.Contains(t, sdata, `"lvl":"ERR","msg":"sync 3"`) + test.Contains(t, sdata, `"msg":"context1","ctx":{"ip":"0.0.0.0"}`) + test.Contains(t, sdata, `"msg":"context2","ctx":{"nil":null}`) + test.Contains(t, sdata, `"msg":"context3","ctx":{"func":"unsupported field value: (func())`) + test.Contains(t, sdata, `"msg":"context4","ctx":{"ip":"0.0.0.0"}`) + test.Contains(t, sdata, `"msg":"context5","ctx":{"nil":null}`) + test.Contains(t, sdata, `"msg":"context6","ctx":{"func":"unsupported field value: (func())`) + test.Contains(t, sdata, `"msg":"context7","ctx":{"err":null}`) + test.Contains(t, sdata, `"msg":"context8","ctx":{"err":"er1"}`) +} + +func TestUnit_NewString(t *testing.T) { + l := New() + + test.NotNil(t, l) + l.SetFormatter(NewFormatString()) + + filename, err := os.CreateTemp(os.TempDir(), "test_new_default-*.log") + test.NoError(t, err) + + l.SetOutput(filename) + l.SetLevel(LevelDebug) + test.Equal(t, LevelDebug, l.GetLevel()) + + go l.Infof("async %d", 1) + go l.Warnf("async %d", 2) + go l.Errorf("async %d", 3) + go l.Debugf("async %d", 4) + + l.Infof("sync %d", 1) + l.Warnf("sync %d", 2) + l.Errorf("sync %d", 3) + l.Debugf("sync %d", 4) + + l.WithFields(Fields{"ip": "0.0.0.0"}).Infof("context1") + l.WithFields(Fields{"nil": nil}).Infof("context2") + l.WithFields(Fields{"func": func() {}}).Infof("context3") + + l.WithField("ip", "0.0.0.0").Infof("context4") + l.WithField("nil", nil).Infof("context5") + l.WithField("func", func() {}).Infof("context6") + + l.WithError("err", nil).Infof("context7") + l.WithError("err", fmt.Errorf("er1")).Infof("context8") + + <-time.After(time.Second * 1) + l.Close() + + test.NoError(t, filename.Close()) + data, err := os.ReadFile(filename.Name()) + test.NoError(t, err) + test.NoError(t, os.Remove(filename.Name())) + + sdata := string(data) + test.Contains(t, sdata, "lvl: INF\tmsg: async 1") + test.Contains(t, sdata, "lvl: WRN\tmsg: async 2") + test.Contains(t, sdata, "lvl: ERR\tmsg: async 3") + test.Contains(t, sdata, "lvl: DBG\tmsg: async 4") + test.Contains(t, sdata, "lvl: INF\tmsg: sync 1") + test.Contains(t, sdata, "lvl: WRN\tmsg: sync 2") + test.Contains(t, sdata, "lvl: ERR\tmsg: sync 3") + test.Contains(t, sdata, "lvl: DBG\tmsg: sync 4") + test.Contains(t, sdata, "msg: context1\tctx: [[ip: 0.0.0.0]]") + test.Contains(t, sdata, "msg: context2\tctx: [[nil: ]]") + test.Contains(t, sdata, "msg: context3\tctx: [[func: unsupported field value: (func())") + test.Contains(t, sdata, "msg: context4\tctx: [[ip: 0.0.0.0]]") + test.Contains(t, sdata, "msg: context5\tctx: [[nil: ]]") + test.Contains(t, sdata, "msg: context6\tctx: [[func: unsupported field value: (func())") + test.Contains(t, sdata, "msg: context7\tctx: [[err: ]]") + test.Contains(t, sdata, "msg: context8\tctx: [[err: er1]]") +} + +func BenchmarkNewJSON(b *testing.B) { + b.ReportAllocs() + + ll := New() + ll.SetOutput(io.Discard) + ll.SetLevel(LevelDebug) + ll.SetFormatter(NewFormatJSON()) + wg := sync.NewGroup() + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + wg.Background(func() { + ll.WithFields(Fields{"a": "b"}).Infof("hello") + ll.WithField("a", "b").Infof("hello") + ll.WithError("a", fmt.Errorf("b")).Infof("hello") + }) + } + }) + wg.Wait() + ll.Close() +} + +func BenchmarkNewString(b *testing.B) { + b.ReportAllocs() + + ll := New() + ll.SetOutput(io.Discard) + ll.SetLevel(LevelDebug) + ll.SetFormatter(NewFormatString()) + wg := sync.NewGroup() + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + wg.Background(func() { + ll.WithFields(Fields{"a": "b"}).Infof("hello") + ll.WithField("a", "b").Infof("hello") + ll.WithError("a", fmt.Errorf("b")).Infof("hello") + }) + } + }) + wg.Wait() + ll.Close() +} diff --git a/log/message.go b/log/message.go new file mode 100644 index 0000000..9ac0880 --- /dev/null +++ b/log/message.go @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package log + +import "sync" + +//go:generate easyjson + +var poolMessage = sync.Pool{ + New: func() interface{} { + return newMessage() + }, +} + +//easyjson:json +type Message struct { + UnixTime int64 `json:"time" yaml:"time"` + Level string `json:"lvl" yaml:"lvl"` + Message string `json:"msg" yaml:"msg"` + Ctx map[string]interface{} `json:"ctx,omitempty" yaml:"ctx,omitempty,inline"` +} + +func newMessage() *Message { + return &Message{ + Ctx: make(map[string]interface{}, 2), + } +} + +func (v *Message) Reset() { + v.UnixTime = 0 + v.Level = "" + v.Message = "" + for s := range v.Ctx { + delete(v.Ctx, s) + } +} diff --git a/log/message_easyjson.go b/log/message_easyjson.go new file mode 100644 index 0000000..a0ec23a --- /dev/null +++ b/log/message_easyjson.go @@ -0,0 +1,150 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package log + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson4086215fDecodeGoOsspkgComGoppyXlog(in *jlexer.Lexer, out *Message) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "time": + out.UnixTime = int64(in.Int64()) + case "lvl": + out.Level = string(in.String()) + case "msg": + out.Message = string(in.String()) + case "ctx": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Ctx = make(map[string]interface{}) + } else { + out.Ctx = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v1 interface{} + if m, ok := v1.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := v1.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + v1 = in.Interface() + } + (out.Ctx)[key] = v1 + in.WantComma() + } + in.Delim('}') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4086215fEncodeGoOsspkgComGoppyXlog(out *jwriter.Writer, in Message) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"time\":" + out.RawString(prefix[1:]) + out.Int64(int64(in.UnixTime)) + } + { + const prefix string = ",\"lvl\":" + out.RawString(prefix) + out.String(string(in.Level)) + } + { + const prefix string = ",\"msg\":" + out.RawString(prefix) + out.String(string(in.Message)) + } + if len(in.Ctx) != 0 { + const prefix string = ",\"ctx\":" + out.RawString(prefix) + { + out.RawByte('{') + v2First := true + for v2Name, v2Value := range in.Ctx { + if v2First { + v2First = false + } else { + out.RawByte(',') + } + out.String(string(v2Name)) + out.RawByte(':') + if m, ok := v2Value.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := v2Value.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(v2Value)) + } + } + out.RawByte('}') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Message) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4086215fEncodeGoOsspkgComGoppyXlog(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Message) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4086215fEncodeGoOsspkgComGoppyXlog(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Message) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4086215fDecodeGoOsspkgComGoppyXlog(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Message) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4086215fDecodeGoOsspkgComGoppyXlog(l, v) +} diff --git a/routine/go.mod b/routine/go.mod new file mode 100644 index 0000000..5a19570 --- /dev/null +++ b/routine/go.mod @@ -0,0 +1,15 @@ +module go.osspkg.com/x/routine + +go 1.20 + +replace ( + go.osspkg.com/x/errors => ../errors + go.osspkg.com/x/sync => ../sync + go.osspkg.com/x/test => ../test +) + +require ( + go.osspkg.com/x/errors v0.5.0 + go.osspkg.com/x/sync v0.5.0 +) + diff --git a/routine/go.sum b/routine/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/routine/routine.go b/routine/routine.go new file mode 100644 index 0000000..fb79a4d --- /dev/null +++ b/routine/routine.go @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package routine + +import ( + "context" + "time" + + "go.osspkg.com/x/errors" + "go.osspkg.com/x/sync" +) + +func Interval(ctx context.Context, interval time.Duration, call func(context.Context)) { + call(ctx) + + go func() { + tick := time.NewTicker(interval) + defer tick.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-tick.C: + call(ctx) + } + } + }() +} + +func Retry(count int, ttl time.Duration, call func() error) error { + var err error + for i := 0; i < count; i++ { + if e := call(); e != nil { + err = errors.Wrap(err, errors.Wrapf(e, "[#%d]", i)) + time.Sleep(ttl) + continue + } + return nil + } + return errors.Wrapf(err, "retry error") +} + +func Repeat(ctx context.Context, call func()) { + call() + for { + select { + case <-ctx.Done(): + return + default: + } + call() + } +} + +func Parallel(calls ...func()) { + wg := sync.NewGroup() + for _, call := range calls { + call := call + wg.Background(func() { + call() + }) + } + wg.Wait() +} diff --git a/routine/routine_test.go b/routine/routine_test.go new file mode 100644 index 0000000..a5a4074 --- /dev/null +++ b/routine/routine_test.go @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2024 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package routine + +import ( + "fmt" + "testing" +) + +func TestUnit_Parallel(t *testing.T) { + Parallel( + func() { + fmt.Println("a") + }, func() { + fmt.Println("b") + }, func() { + fmt.Println("c") + }, + ) +} diff --git a/sync/go.mod b/sync/go.mod index 834fa42..674dab8 100644 --- a/sync/go.mod +++ b/sync/go.mod @@ -4,4 +4,4 @@ go 1.20 replace go.osspkg.com/x/test => ../test -require go.osspkg.com/x/test v0.3.0 +require go.osspkg.com/x/test v0.5.0