revamp: start simple

This commit is contained in:
Suyono 2023-12-06 17:01:43 +11:00
parent 4f54db3cbd
commit dbb703db61
28 changed files with 214 additions and 1381 deletions

View File

@ -0,0 +1,4 @@
{
"name": "Golang Dev",
"image": "golang:1.21-bookworm"
}

View File

@ -1,34 +0,0 @@
package cmd
import (
"os"
initpkg "gitea.suyono.dev/suyono/wingmate/cmd/init"
"gitea.suyono.dev/suyono/wingmate/config"
"github.com/spf13/cobra"
)
var (
rootCmd = &cobra.Command{
Use: "wingmate",
Short: "your service companion",
}
)
func init() {
config.BindFlags(rootCmd)
rootCmd.AddCommand(
initpkg.Command(),
)
}
func Execute() error {
if os.Args[0] != rootCmd.Use {
rootCmd.SetArgs(os.Args)
} else {
rootCmd.SetArgs(os.Args[1:])
}
return rootCmd.Execute()
}

View File

@ -1,21 +0,0 @@
package init
import (
"gitea.suyono.dev/suyono/wingmate/config"
"gitea.suyono.dev/suyono/wingmate/daemon"
"gitea.suyono.dev/suyono/wingmate/middleware"
"github.com/spf13/cobra"
)
func Command() *cobra.Command {
init := &cobra.Command{
Use: "init",
Short: "dummy init",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return middleware.CallChain(cmd, args, config.Read, daemon.Start)
},
}
return init
}

5
cmd/wingmate/wingmate.go Normal file
View File

@ -0,0 +1,5 @@
package main
func main() {
}

View File

@ -1,84 +1 @@
package config
import (
"fmt"
"gitea.suyono.dev/suyono/wingmate/debugframes"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"path"
)
const (
EnvPrefix = "wingmate"
PathKey = "config_path"
SearchPathKey = "config_search_path"
defaultPath = "/etc/wingmate/"
defaultName = "config"
ServiceKey = "service"
CronKey = "cron"
CommandKey = "command"
)
var (
configPath string
configSearchPath []string
configRead bool
)
func init() {
viper.SetEnvPrefix(EnvPrefix)
_ = viper.BindEnv(PathKey)
_ = viper.BindEnv(SearchPathKey)
}
func BindFlags(command *cobra.Command) {
command.PersistentFlags().StringVarP(&configPath, "config", "c", defaultPath+defaultName+".yml", "configuration path")
command.PersistentFlags().StringSliceVar(&configSearchPath, "config-dir", []string{}, "configuration search path")
}
func Read(cmd *cobra.Command, args []string) error {
var (
err error
)
_, _ = cmd, args // prevent warning for unused arguments
if viper.IsSet(PathKey) {
configPath = viper.GetString(PathKey)
}
if viper.IsSet(SearchPathKey) {
if err = viper.UnmarshalKey(SearchPathKey, &configSearchPath); err != nil {
return fmt.Errorf("reading %s: %w %w", SearchPathKey, err, debugframes.GetTraces())
}
}
if configRead {
return nil
}
viper.SetConfigType("yaml")
if len(configSearchPath) > 0 {
name := path.Base(configPath)
dir := path.Dir(configPath)
if dir != "." {
configSearchPath = append([]string{dir}, configSearchPath...)
}
viper.SetConfigName(name)
for _, p := range configSearchPath {
viper.AddConfigPath(p)
}
} else {
viper.SetConfigFile(configPath)
}
if err = viper.ReadInConfig(); err != nil {
return fmt.Errorf("reading config: %w %w", err, debugframes.GetTraces())
}
configRead = true
return nil
}

View File

@ -1,191 +0,0 @@
package config
import (
"gitea.suyono.dev/suyono/wingmate/files/testconfig"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os"
"strings"
"testing"
)
type testReadArgs struct {
cmd *cobra.Command
args []string
}
type testRead struct {
name string
env map[string]string
args testReadArgs
wantErr bool
pre func(t *testing.T, tc *testRead)
post func(t *testing.T, tc *testRead)
}
func TestRead(t *testing.T) {
type args struct {
cmd *cobra.Command
args []string
}
tests := []testRead{
{
name: "env",
env: map[string]string{
strings.ToUpper(EnvPrefix + "_" + PathKey): "/path/to/config",
strings.ToUpper(EnvPrefix + "_" + SearchPathKey): "/path/one,/path/two",
},
args: testReadArgs{
nil,
[]string{},
},
wantErr: true,
},
{
name: "env exist",
args: testReadArgs{
nil,
[]string{},
},
wantErr: false,
pre: func(t *testing.T, tc *testRead) {
var (
f *os.File
err error
fname string
)
if f, err = os.CreateTemp("", "config-*.yml"); err != nil {
t.Fatal("create temp:", err)
}
fname = f.Name()
if _, err = f.WriteString(testconfig.One); err != nil {
t.Fatal("writing temp:", err)
}
if err = f.Close(); err != nil {
t.Fatal("closing temp:", err)
}
tc.env = map[string]string{
strings.ToUpper(EnvPrefix + "_" + PathKey): fname,
}
tc.post = func(t *testing.T, tc *testRead) {
_ = os.Remove(fname)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var err error
if tt.pre != nil {
tt.pre(t, &tt)
}
if tt.post != nil {
defer tt.post(t, &tt)
}
for k, v := range tt.env {
if err = os.Setenv(k, v); err != nil {
t.Fatal("failed", err)
}
}
defer func() {
for k := range tt.env {
if err = os.Unsetenv(k); err != nil {
t.Fatal("failed", err)
}
}
}()
if err = Read(tt.args.cmd, tt.args.args); (err != nil) != tt.wantErr {
t.Fatalf("Read() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
t.Log("case:", tt.name, "; expected error:", err)
}
})
}
}
func TestGet(t *testing.T) {
type args struct {
cmd *cobra.Command
args []string
}
tests := []testRead{
{
name: "env exist",
args: testReadArgs{
nil,
[]string{},
},
wantErr: false,
pre: func(t *testing.T, tc *testRead) {
var (
f *os.File
err error
fname string
)
if f, err = os.CreateTemp("", "config-*.yml"); err != nil {
t.Fatal("create temp:", err)
}
fname = f.Name()
if _, err = f.WriteString(testconfig.One); err != nil {
t.Fatal("writing temp:", err)
}
if err = f.Close(); err != nil {
t.Fatal("closing temp:", err)
}
tc.env = map[string]string{
strings.ToUpper(EnvPrefix + "_" + PathKey): fname,
}
tc.post = func(t *testing.T, tc *testRead) {
_ = os.Remove(fname)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var err error
if tt.pre != nil {
tt.pre(t, &tt)
}
if tt.post != nil {
defer tt.post(t, &tt)
}
for k, v := range tt.env {
if err = os.Setenv(k, v); err != nil {
t.Fatal("failed", err)
}
}
defer func() {
for k := range tt.env {
if err = os.Unsetenv(k); err != nil {
t.Fatal("failed", err)
}
}
}()
if err = Read(tt.args.cmd, tt.args.args); err != nil {
t.Fatal("fail to read config:", err)
}
t.Log(viper.AllKeys())
m := viper.GetStringMap(ServiceKey)
for s := range m {
t.Log(s)
t.Log(viper.GetStringMap(ServiceKey + "." + s))
}
})
}
}

View File

@ -1,109 +0,0 @@
package daemon
import (
"fmt"
"gitea.suyono.dev/suyono/wingmate/config"
"gitea.suyono.dev/suyono/wingmate/debugframes"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os/exec"
"runtime/debug"
)
type InstanceType int
type WingMateDescriptor interface {
String() string
Name() string
InstanceName() string
InstanceType() InstanceType
Error() string // so the descriptor can be wrapped inside an error
}
type process struct {
cmd *exec.Cmd
descriptor WingMateDescriptor
}
type daemon struct {
running map[string]process
services map[string]*service
cron map[string]WingMateDescriptor
logChannel chan<- any
errorChannel chan<- error
}
const (
Service InstanceType = iota
Cron
)
func (t InstanceType) String() string {
switch t {
case Service:
return config.ServiceKey
case Cron:
return config.CronKey
default:
return "[unknown instance type]"
}
}
func Start(cmd *cobra.Command, args []string) (err error) {
defer func() {
if o := recover(); o != nil {
if err != nil {
err = fmt.Errorf("panic: %v %w and error: %w", o, debugframes.PanicTrace(debug.Stack()), err)
} else {
err = fmt.Errorf("panic: %v %w", o, debugframes.PanicTrace(debug.Stack()))
}
}
}()
_, _ = cmd, args // prevent warnings for unused arguments
_, err = start()
return
}
func start() (*daemon, error) {
var (
err error
k string
svc *service
cmd *exec.Cmd
)
d := &daemon{
running: make(map[string]process),
services: make(map[string]*service),
cron: make(map[string]WingMateDescriptor),
logChannel: make(chan<- any, 16),
errorChannel: make(chan<- error, 4),
}
if err = d.buildServiceMap(); err != nil {
return nil, err
}
for k, svc = range d.services {
if cmd, err = StartProcess(svc); err != nil {
return nil, err //TODO: this is not supposed to return, log and start the next service
}
d.running[k] = process{
cmd: cmd,
descriptor: svc,
}
}
//TODO: create loop to receive and process log
//TODO: create loop to receive error
//TODO: create signal handler
return d, nil
}
func (d *daemon) buildServiceMap() error {
for s := range viper.GetStringMap(Service.String()) {
d.services[s] = newService(s, d.errorChannel, d.logChannel)
}
return nil
}

View File

@ -1,123 +0,0 @@
package daemon
import (
"bufio"
"fmt"
"gitea.suyono.dev/suyono/wingmate/debugframes"
"io"
"os"
"os/exec"
"runtime/debug"
)
type ProcessConfig interface {
Descriptor() WingMateDescriptor
// ExecutableName returns the executable/binary name to be executed, to be passed in to exec.Command
ExecutableName() string
// Args returns the arguments for the process, to be passed in to exec.Command
Args() []string
Env() map[string]string
WorkingDir() string
LogChannel() chan<- any
ErrorChannel() chan<- error
}
type StreamID int
type ProcessLogEntry struct {
Descriptor WingMateDescriptor
LogEntry string
}
const (
Stdout StreamID = iota
Stderr
)
func (s StreamID) String() string {
switch s {
case Stdout:
return "stdout"
case Stderr:
return "stderr"
default:
return "[unknown stream]"
}
}
func StartProcess(config ProcessConfig) (*exec.Cmd, error) {
var (
err error
cmd *exec.Cmd
k, v string
stdoutPipe, stderrPipe io.ReadCloser
)
cmd = exec.Command(config.ExecutableName(), config.Args()...)
cmd.Env = os.Environ()
for k, v = range config.Env() {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
}
cmd.Dir = config.WorkingDir()
if stdoutPipe, err = cmd.StdoutPipe(); err != nil {
return nil, fmt.Errorf("set up stdout pipe on process %w: %w %w",
config.Descriptor(), err, debugframes.GetTraces())
}
if stderrPipe, err = cmd.StderrPipe(); err != nil {
return nil, fmt.Errorf("set up stderr pipe on process %w: %w %w",
config.Descriptor(), err, debugframes.GetTraces())
}
if err = cmd.Start(); err != nil {
return nil, fmt.Errorf("starting process %w: %w %w", config.Descriptor(), err, debugframes.GetTraces())
}
go serviceLogReader(config, stdoutPipe, Stdout)
go serviceLogReader(config, stderrPipe, Stderr)
go serviceWaiter(config, cmd)
return cmd, nil
}
func serviceLogReader(config ProcessConfig, stream io.Reader, id StreamID) {
out := config.LogChannel()
eChan := config.ErrorChannel()
defer func() {
if o := recover(); o != nil {
eChan <- fmt.Errorf("log reader %s of %w panic: %v %w",
id.String(), config.Descriptor(), o, debugframes.PanicTrace(debug.Stack()))
} else {
eChan <- fmt.Errorf("log reader %s of %w returned", id.String(), config.Descriptor())
}
}()
scanner := bufio.NewScanner(stream)
for scanner.Scan() {
out <- ProcessLogEntry{
Descriptor: config.Descriptor(),
LogEntry: scanner.Text(),
}
}
if err := scanner.Err(); err != nil {
eChan <- fmt.Errorf("read %s from %w: %w %w",
id.String(), config.Descriptor(), err, debugframes.GetTraces())
}
}
func serviceWaiter(config ProcessConfig, cmd *exec.Cmd) {
eChan := config.ErrorChannel()
defer func() {
if o := recover(); o != nil {
eChan <- fmt.Errorf("%w panic: %v %w", config.Descriptor(), o, debugframes.PanicTrace(debug.Stack()))
}
}()
if err := cmd.Wait(); err != nil {
eChan <- fmt.Errorf("wait error on service %w: %w", config.Descriptor(), err)
} else {
eChan <- config.Descriptor()
}
}

View File

@ -1,12 +0,0 @@
package daemon
import "golang.org/x/sys/unix"
const (
anyChildProcess = -1
)
func WaitChildProcess() (int, error) {
var ws unix.WaitStatus
return unix.Wait4(anyChildProcess, &ws, 0, nil)
}

View File

@ -1,49 +0,0 @@
package daemon
import (
"os"
"os/exec"
"testing"
"time"
"github.com/stretchr/testify/assert"
"golang.org/x/sys/unix"
)
func TestWait(t *testing.T) {
var (
err error
pid int
testpid int
)
cmd := exec.Command("sleep", "10")
if err = cmd.Start(); err != nil {
t.Fatal("failed to start command:", err)
}
pid = cmd.Process.Pid
t.Log("started pid:", pid)
go func() {
time.Sleep(1200 * time.Millisecond)
p, terr := os.FindProcess(pid)
if terr == nil {
if terr = p.Signal(unix.SIGTERM); terr != nil {
t.Log("sending signal failed:", terr)
}
} else {
t.Logf("find process %d: %v", pid, terr)
}
}()
testpid, err = WaitChildProcess()
if err != nil {
t.Fatal("wait4 error:", err)
}
//if err = cmd.Wait(); err != nil {
// t.Fatal("is this expected?", err)
//}
assert.Equal(t, pid, testpid)
}

View File

@ -1,91 +0,0 @@
package daemon
import (
"fmt"
"io"
"gitea.suyono.dev/suyono/wingmate/config"
"github.com/spf13/viper"
)
type service struct {
name string
stdoutLogWriter io.WriteCloser
stderrLogWriter io.WriteCloser
logChannel chan<- any
errChannel chan<- error
}
func newService(name string, error chan<- error, logging chan<- any) *service {
return &service{
name: name,
// stdoutLogWriter: log.NewLogFile(fmt.Sprintf("%s.%s.%s", Service.String(), name, config.CommandKey)),
logChannel: logging,
errChannel: error,
}
}
//TODO: review back the implementation of service as WingMateDescriptor
func (s *service) String() string {
return s.name
}
func (s *service) Name() string {
return s.name
}
func (s *service) InstanceName() string {
return s.name
}
func (s *service) InstanceType() InstanceType {
return Service
}
func (s *service) Error() string {
return s.name
}
func (s *service) parseCommand() []string {
key := fmt.Sprintf("%s.%s.%s", Service.String(), s.name, config.CommandKey)
if !viper.IsSet(key) {
panic(fmt.Errorf("parse command: key %s is not set", key))
}
result := viper.GetStringSlice(key)
if len(result) < 1 {
panic(fmt.Errorf("parse command: zero command length"))
}
return result
}
func (s *service) ExecutableName() string {
return s.parseCommand()[0]
}
func (s *service) Args() []string {
return s.parseCommand()[1:]
}
func (s *service) Descriptor() WingMateDescriptor {
return s
}
func (s *service) Env() map[string]string {
//TODO: stub implementation; FIX!
return make(map[string]string)
}
func (s *service) WorkingDir() string {
//TODO: stub implementation; FIX!
return ""
}
func (s *service) LogChannel() chan<- any {
return s.logChannel
}
func (s *service) ErrorChannel() chan<- error {
return s.errChannel
}

View File

@ -1,82 +0,0 @@
package daemon
import (
"gitea.suyono.dev/suyono/wingmate/config"
"gitea.suyono.dev/suyono/wingmate/files/testconfig"
"github.com/spf13/viper"
"os"
"strings"
"testing"
)
type parseCommandTestCase struct {
name string
pre func(t *testing.T, tt *parseCommandTestCase)
post func(t *testing.T, tt *parseCommandTestCase)
}
func TestParseCommand(t *testing.T) {
tests := []parseCommandTestCase{
{
name: "initial",
pre: func(t *testing.T, tt *parseCommandTestCase) {
var (
f *os.File
err error
fname, key string
)
if f, err = os.CreateTemp("", "config-*.yml"); err != nil {
t.Fatal("create temp:", err)
}
fname = f.Name()
if _, err = f.WriteString(testconfig.One); err != nil {
t.Fatal("writing temp:", err)
}
if err = f.Close(); err != nil {
t.Fatal("closing temp:", err)
}
key = strings.ToUpper(config.EnvPrefix + "_" + config.PathKey)
if err = os.Setenv(key, fname); err != nil {
t.Fatal("set up env failed", err)
}
tt.post = func(t *testing.T, tt *parseCommandTestCase) {
_ = os.Unsetenv(key)
_ = os.Remove(fname)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
err error
d *daemon
s *service
)
if tt.pre != nil {
tt.pre(t, &tt)
}
if tt.post != nil {
defer tt.post(t, &tt)
}
if err = config.Read(nil, []string{}); err != nil {
t.Fatal("reading config", err)
}
if d, err = start(); err != nil {
t.Fatal("starting daemon", err)
}
for _, s = range d.services {
s.parseCommand()
}
x := viper.GetStringSlice("non.existent")
t.Log(x)
})
}
}

View File

@ -1,35 +0,0 @@
package debugframes
import (
"gitea.suyono.dev/suyono/wingmate/log"
"runtime"
)
type Trace struct {
Frames []runtime.Frame
}
func (t *Trace) Error() string {
return ""
}
func (t *Trace) AppendFields(h log.FieldsHandler) log.FieldsHandler {
return h.HandleFields("trace", t.Frames)
}
func GetTraces() error {
trace := new(Trace)
pc := make([]uintptr, 16)
if n := runtime.Callers(2, pc); n > 0 {
frames := runtime.CallersFrames(pc[:n])
for {
frame, more := frames.Next()
trace.Frames = append(trace.Frames, frame)
if !more {
break
}
}
}
return trace
}

View File

@ -1,18 +0,0 @@
package debugframes
import "gitea.suyono.dev/suyono/wingmate/log"
type PanicTrace []byte
func (p PanicTrace) Error() string {
return ""
}
func (p PanicTrace) AppendFields(h log.FieldsHandler) log.FieldsHandler {
return h.HandleFields("panic_stacktrace", p.String())
}
func (p PanicTrace) String() string {
b := []byte(p)
return string(b)
}

7
init/cron.go Normal file
View File

@ -0,0 +1,7 @@
package init
import "sync"
func (i *Init) cron(wg *sync.WaitGroup, cron Cron) {
defer wg.Done()
}

80
init/init.go Normal file
View File

@ -0,0 +1,80 @@
package init
import (
"gitea.suyono.dev/suyono/wingmate"
"sync"
)
type Path interface {
Path() string
}
type CronExactSpec interface {
CronTimeSpec
Value() uint8
}
type CronMultipleOccurrenceSpec interface {
CronTimeSpec
MultipleValues() []uint8
}
type CronTimeSpec interface {
Type() wingmate.CronTimeType
}
type Cron interface {
Minute() CronTimeSpec
Hour() CronTimeSpec
DayOfMonth() CronTimeSpec
Month() CronTimeSpec
DayOfWeek() CronTimeSpec
Command() Path
}
type Config interface {
Services() []Path
Cron() []Cron
}
type Init struct {
config Config
}
func NewInit(config Config) *Init {
return &Init{
config: config,
}
}
func (i *Init) Start() {
var (
wg *sync.WaitGroup
signalTrigger chan any
sighandlerExit chan any
)
signalTrigger = make(chan any)
sighandlerExit = make(chan any)
wg = &sync.WaitGroup{}
wg.Add(1)
go i.sighandler(wg, signalTrigger, sighandlerExit)
for _, s := range i.config.Services() {
wg.Add(1)
go func(p Path) {
for {
if err := i.service(wg, p); err != nil {
wingmate.Log().Error().Msgf("starting service %s error %#v", p.Path(), err)
}
}
}(s)
}
for _, c := range i.config.Cron() {
wg.Add(1)
go i.cron(wg, c)
}
wg.Wait()
}

21
init/service.go Normal file
View File

@ -0,0 +1,21 @@
package init
import (
"os/exec"
"sync"
)
func (i *Init) service(wg *sync.WaitGroup, path Path) error {
defer wg.Done()
var (
err error
)
cmd := exec.Command(path.Path())
if err = cmd.Run(); err != nil {
return err
}
return nil
}

31
init/sighandler.go Normal file
View File

@ -0,0 +1,31 @@
package init
import (
"golang.org/x/sys/unix"
"os"
"os/signal"
"sync"
)
func (i *Init) sighandler(wg *sync.WaitGroup, trigger chan<- any, selfExit <-chan any) {
defer wg.Wait()
c := make(chan os.Signal, 1)
signal.Notify(c, unix.SIGINT, unix.SIGTERM, unix.SIGCHLD)
signal:
for {
select {
case s := <-c:
switch s {
case unix.SIGTERM, unix.SIGINT:
close(trigger)
case unix.SIGCHLD:
// do nothing
}
case <-selfExit:
break signal
}
}
}

View File

@ -1,27 +0,0 @@
package log
import (
"errors"
"github.com/rs/zerolog"
"testing"
)
func TestPrint(t *testing.T) {
Print("key", errors.New("hello world"))
}
type testWriter struct {
}
func (t testWriter) Write(b []byte) (int, error) {
return 0, errors.New("write error")
}
func TestErrWrite(t *testing.T) {
var (
tw testWriter
logger zerolog.Logger
)
logger = zerolog.New(tw)
logger.Error().Str("test", "one").Msg("hello")
}

View File

@ -1,52 +0,0 @@
package log
import (
"fmt"
"os"
"github.com/spf13/viper"
)
type File os.File
const (
filenameKey = "file"
)
func NewLogFile(configKey string) (*File, error) {
var (
err error
path string
file *os.File
)
if viper.IsSet(configKey) {
return nil, nil
}
fileKey := fmt.Sprintf("%s.%s", configKey, filenameKey)
if !viper.IsSet(fileKey) {
return nil, fmt.Errorf("missing file key config %s", fileKey)
}
path = viper.GetString(fileKey)
if file, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
return nil, err
}
return (*File)(file), nil
}
func (f *File) Write(b []byte) (int, error) {
file := (*os.File)(f)
return file.Write(b)
}
func (f *File) Close() error {
of := (*os.File)(f)
return of.Close()
}
func (f *File) Rotate() error {
//TODO: implement log rotation
return nil
}

View File

@ -1,306 +0,0 @@
package log
import (
"errors"
"fmt"
"io"
"os"
"reflect"
"runtime/debug"
"github.com/rs/zerolog"
)
type Level zerolog.Level
type Payload zerolog.Event
type ProcessLogConfig interface {
Method() string
FileName() string
}
type Traceable interface {
error
Trace() []string
}
type FieldsHandler interface {
HandleFields(a ...any) FieldsHandler
}
type ErrorLogFields interface {
error
AppendFields(fh FieldsHandler) FieldsHandler
}
const (
Trace Level = Level(zerolog.TraceLevel)
Debug Level = Level(zerolog.DebugLevel)
Info Level = Level(zerolog.InfoLevel)
Warn Level = Level(zerolog.WarnLevel)
Error Level = Level(zerolog.ErrorLevel)
Fatal Level = Level(zerolog.FatalLevel)
Panic Level = Level(zerolog.PanicLevel)
LogKey = "log"
)
var (
globalLevel Level = Error
logger zerolog.Logger
onLogWriterError zerolog.Logger
writer *Writer
DefaultOutlet io.Writer
errType reflect.Type
errInvalidType error = errors.New("invalid type")
errNoValidField error = errors.New("no valid field")
)
func init() {
errType = reflect.TypeOf((*error)(nil)).Elem()
DefaultOutlet = os.Stderr
writer = NewWriter()
logger = zerolog.New(writer)
onLogWriterError = logger.Output(DefaultOutlet)
SetGlobalLevel(globalLevel)
}
func (l Level) Print(a ...any) {
l.Fields(a...).Send()
}
func (l Level) PrintTo(outlets []io.Writer, a ...any) {
l.Fields(a...).Send(outlets...)
}
func (l Level) Fields(a ...any) (p *Payload) {
return payload(logger.WithLevel(zerolog.Level(l)).Timestamp()).Fields(a...)
}
func (p *Payload) HandleFields(a ...any) FieldsHandler {
return p.Fields(a...)
}
func (p *Payload) Fields(a ...any) (result *Payload) {
var (
event *zerolog.Event
sub *zerolog.Event
key string
val reflect.Value
typ reflect.Type
arr *zerolog.Array
err error
errVal error
elf ErrorLogFields
)
event = p.event()
defer func() {
if o := recover(); o != nil {
// in case there is mishandling reflect
result = payload(event.Str("logger_panic", string(debug.Stack())))
}
}()
enumerateFields:
for i, part := range a {
if i%2 == 0 {
key = part.(string)
} else {
val = reflect.ValueOf(part)
typ = val.Type()
if typ.Implements(errType) {
errVal = val.Interface().(error)
event = event.Err(errVal)
if errors.As(errVal, &elf) {
event = elf.AppendFields(payload(event)).(*Payload).event()
}
continue enumerateFields
}
switch typ.Kind() {
case reflect.String:
event = event.Str(key, val.String())
case reflect.Slice, reflect.Array:
if arr, err = p.slice(val); err != nil {
event = payload(event).unhandledField(key, part)
} else {
event = event.Array(key, arr)
}
case reflect.Bool:
event = event.Bool(key, val.Bool())
case reflect.Struct:
if sub, err = p.structType(val); err != nil {
event = payload(event).unhandledField(key, part)
} else {
event = event.Dict(key, sub)
}
case reflect.Pointer:
if sub, err = p.pointer(val); err != nil {
event = payload(event).unhandledField(key, part)
} else {
event = event.Dict(key, sub)
}
case reflect.Int:
event = event.Int(key, int(val.Int()))
case reflect.Int8:
event = event.Int8(key, int8(val.Int()))
case reflect.Int16:
event = event.Int16(key, int16(val.Int()))
case reflect.Int32:
event = event.Int32(key, int32(val.Int()))
case reflect.Int64:
event = event.Int64(key, val.Int())
case reflect.Float32:
event = event.Float32(key, float32(val.Float()))
case reflect.Float64:
event = event.Float64(key, val.Float())
case reflect.Uint:
event = event.Uint(key, uint(val.Uint()))
case reflect.Uint8:
event = event.Uint8(key, uint8(val.Uint()))
case reflect.Uint16:
event = event.Uint16(key, uint16(val.Uint()))
case reflect.Uint32:
event = event.Uint32(key, uint32(val.Uint()))
case reflect.Uint64:
event = event.Uint64(key, val.Uint())
default:
event = payload(event).unhandledField(key, part)
}
}
}
return payload(event)
}
func (p *Payload) Send(ws ...io.Writer) {
event := (*zerolog.Event)(p)
writer.Lock()
defer writer.Unlock()
if len(ws) == 0 {
writer.Writers(DefaultOutlet)
} else {
writer.Writers(ws...)
}
event.Send()
}
func (p *Payload) unhandledField(key string, v any) *zerolog.Event {
event := (*zerolog.Event)(p)
return event.Str(key, fmt.Sprintf("[unhandled data type / invalid] %+v", v))
}
func (p *Payload) slice(val reflect.Value) (*zerolog.Array, error) {
var (
vlen int
i int
rval *zerolog.Array
event *zerolog.Event
item reflect.Value
err error
)
vlen = val.Len()
rval = zerolog.Arr()
for i = 0; i < vlen; i++ {
item = val.Index(i)
switch item.Kind() {
//TODO: handle more cases
case reflect.String:
rval = rval.Str(item.String())
case reflect.Struct:
if event, err = p.structType(item); err != nil {
return nil, err
}
rval = rval.Dict(event)
case reflect.Int:
rval = rval.Int(int(item.Int()))
case reflect.Int8:
rval = rval.Int8(int8(item.Int()))
case reflect.Int16:
rval = rval.Int16(int16(item.Int()))
case reflect.Int32:
rval = rval.Int32(int32(item.Int()))
case reflect.Int64:
rval = rval.Int64(item.Int())
case reflect.Float32:
rval = rval.Float32(float32(item.Float()))
case reflect.Float64:
rval = rval.Float64(item.Float())
case reflect.Uint:
rval = rval.Uint(uint(item.Uint()))
case reflect.Uint8:
rval = rval.Uint8(uint8(item.Uint()))
case reflect.Uint16:
rval = rval.Uint16(uint16(item.Uint()))
case reflect.Uint32:
rval = rval.Uint32(uint32(item.Uint()))
case reflect.Uint64:
rval = rval.Uint64(item.Uint())
default:
rval = rval.Str(fmt.Sprintf("[unhandled data type / invalid] %+v", item.Interface()))
}
}
return rval, nil
}
// pointer only supports pointer to a struct
func (p *Payload) pointer(val reflect.Value) (*zerolog.Event, error) {
val = val.Elem()
if val.Kind() != reflect.Struct {
return nil, errInvalidType
}
return p.structType(val)
}
func (p *Payload) structType(val reflect.Value) (*zerolog.Event, error) {
var (
typ reflect.Type
event *zerolog.Event
gotValidField bool
)
typ = val.Type()
event = zerolog.Dict()
for _, sf := range reflect.VisibleFields(typ) {
switch sf.Type.Kind() {
case reflect.String:
event = event.Str(sf.Name, val.FieldByIndex(sf.Index).String())
gotValidField = true
case reflect.Int:
event = event.Int64(sf.Name, val.FieldByIndex(sf.Index).Int())
gotValidField = true
}
}
if !gotValidField {
return nil, errNoValidField
}
return event, nil
}
func (p *Payload) event() *zerolog.Event {
return (*zerolog.Event)(p)
}
func payload(event *zerolog.Event) *Payload {
return (*Payload)(event)
}
func Print(a ...any) {
globalLevel.Print(a...)
}
func PrintTo(outlets []io.Writer, a ...any) {
globalLevel.PrintTo(outlets, a...)
}
func SetGlobalLevel(l Level) {
globalLevel = l
zerolog.SetGlobalLevel(zerolog.Level(globalLevel))
}

View File

@ -1,29 +0,0 @@
package wrapped
import (
"fmt"
"gitea.suyono.dev/suyono/wingmate/debugframes"
"gitea.suyono.dev/suyono/wingmate/log"
"runtime/debug"
"testing"
)
func TestPrintWrappedError(t *testing.T) {
log.Print("test", fmt.Errorf("wrapped error trace%w", debugframes.GetTraces()))
}
func TestPrintWrappedPanic(t *testing.T) {
f := func() (err error) {
defer func() {
if o := recover(); o != nil {
err = fmt.Errorf("panic: %v%w", o, debugframes.PanicTrace(debug.Stack()))
}
}()
panic("test")
return nil
}
err := f()
log.Print("panic_test", err)
}

View File

@ -1,90 +0,0 @@
package log
import (
"errors"
"fmt"
"io"
"sync"
)
var (
noError = errors.New("no error")
ec = make(chan error)
)
type Writer struct {
writers []io.Writer
mtx *sync.Mutex
}
func NewWriter() *Writer {
return &Writer{
writers: make([]io.Writer, 0),
mtx: &sync.Mutex{},
}
}
func (w *Writer) Write(b []byte) (int, error) {
var (
ctr int
sw io.Writer
err error
)
for _, ww := range w.writers {
if ww == DefaultOutlet {
sw = ww
} else {
ctr++
go subWriter(ww, b, ec)
}
}
if sw != nil {
_, _ = sw.Write(b)
}
for i := 0; i < ctr; i++ {
if err = <-ec; err != noError {
onLogWriterError.Error().Err(err).Msg("subWriter error")
}
}
return len(b), nil
}
func (w *Writer) Lock() {
w.mtx.Lock()
}
func (w *Writer) Unlock() {
w.writers[0] = DefaultOutlet
w.writers = w.writers[:1]
w.mtx.Unlock()
}
func (w *Writer) Writers(ws ...io.Writer) {
if len(ws) == 0 {
w.writers[0] = DefaultOutlet
w.writers = w.writers[:1]
} else {
w.writers = append(w.writers, ws...)
}
}
func subWriter(w io.Writer, b []byte, errchan chan<- error) {
defer func() {
if o := recover(); o != nil {
errchan <- fmt.Errorf("subWriter panic: %v", o)
}
}()
if _, err := w.Write(b); err != nil {
errchan <- err
} else {
errchan <- noError
}
}
//func (w *Writer) Close() {
//}

41
logger.go Normal file
View File

@ -0,0 +1,41 @@
package wingmate
import (
"gitea.suyono.dev/suyono/wingmate/logger"
"github.com/rs/zerolog"
"io"
)
var (
w *wrapper
)
type wrapper struct {
log zerolog.Logger
}
func NewLog(wc io.WriteCloser) error {
w = &wrapper{
log: zerolog.New(wc),
}
return nil
}
func Log() logger.Log {
if w == nil {
panic("nil internal logger")
}
return w
}
func (w *wrapper) Info() logger.Content {
return w.log.Info()
}
func (w *wrapper) Warn() logger.Content {
return w.log.Warn()
}
func (w *wrapper) Error() logger.Content {
return w.log.Error()
}

16
logger/logger.go Normal file
View File

@ -0,0 +1,16 @@
package logger
type Content interface {
Msg(string)
Msgf(string, ...any)
}
type Level interface {
Info() Content
Warn() Content
Error() Content
}
type Log interface {
Level
}

View File

@ -1,7 +0,0 @@
package main
import "gitea.suyono.dev/suyono/wingmate/cmd"
func main() {
_ = cmd.Execute()
}

View File

@ -1,22 +0,0 @@
package middleware
import (
"fmt"
"github.com/spf13/cobra"
)
type ChainFunc func(cmd *cobra.Command, args []string) error
func CallChain(cmd *cobra.Command, args []string, functions ...ChainFunc) error {
var (
err error
)
for i, f := range functions {
if err = f(cmd, args); err != nil {
return fmt.Errorf("error when calling function number %d: %w", i, err)
}
}
return nil
}

9
types.go Normal file
View File

@ -0,0 +1,9 @@
package wingmate
type CronTimeType int
const (
Any CronTimeType = iota
Exact
MultipleOccurrence
)