migrated from logrus to zerolog, improved error formatting, fixed concurrent map write, fixed crash on rapid page refresh for idle containers, fixed infinite recursion on gotfiy error, fixed websocket connection problem when using idlewatcher

This commit is contained in:
yusing
2024-10-29 11:34:58 +08:00
parent cfa74d69ae
commit e5bbb18414
137 changed files with 2640 additions and 2348 deletions

View File

@@ -1,10 +1,10 @@
package watcher
import (
"context"
"sync"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/task"
)
var (
@@ -17,7 +17,7 @@ func NewConfigFileWatcher(filename string) Watcher {
configDirWatcherMu.Lock()
defer configDirWatcherMu.Unlock()
if configDirWatcher == nil {
configDirWatcher = NewDirectoryWatcher(context.Background(), common.ConfigBasePath)
configDirWatcher = NewDirectoryWatcher(task.GlobalTask("config watcher"), common.ConfigBasePath)
}
return configDirWatcher.Add(filename)
}

View File

@@ -7,13 +7,16 @@ import (
"sync"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/task"
F "github.com/yusing/go-proxy/internal/utils/functional"
"github.com/yusing/go-proxy/internal/watcher/events"
)
type DirWatcher struct {
zerolog.Logger
dir string
w *fsnotify.Watcher
@@ -23,7 +26,7 @@ type DirWatcher struct {
eventCh chan Event
errCh chan E.Error
ctx context.Context
task task.Task
}
// NewDirectoryWatcher returns a DirWatcher instance.
@@ -34,22 +37,26 @@ type DirWatcher struct {
//
// Note that the returned DirWatcher is not ready to use until the goroutine
// started by NewDirectoryWatcher has finished.
func NewDirectoryWatcher(ctx context.Context, dirPath string) *DirWatcher {
func NewDirectoryWatcher(callerSubtask task.Task, dirPath string) *DirWatcher {
//! subdirectories are not watched
w, err := fsnotify.NewWatcher()
if err != nil {
logrus.Panicf("unable to create fs watcher: %s", err)
logger.Panic().Err(err).Msg("unable to create fs watcher")
}
if err = w.Add(dirPath); err != nil {
logrus.Panicf("unable to create fs watcher: %s", err)
logger.Panic().Err(err).Msg("unable to create fs watcher")
}
helper := &DirWatcher{
Logger: logger.With().
Str("type", "dir").
Str("path", dirPath).
Logger(),
dir: dirPath,
w: w,
fwMap: F.NewMapOf[string, *fileWatcher](),
eventCh: make(chan Event),
errCh: make(chan E.Error),
ctx: ctx,
task: callerSubtask,
}
go helper.start()
return helper
@@ -73,14 +80,10 @@ func (h *DirWatcher) Add(relPath string) Watcher {
eventCh: make(chan Event),
errCh: make(chan E.Error),
}
go func() {
defer func() {
close(s.eventCh)
close(s.errCh)
}()
<-h.ctx.Done()
logrus.Debugf("file watcher %s stopped", relPath)
}()
h.task.OnFinished("close file watcher for "+relPath, func() {
close(s.eventCh)
close(s.errCh)
})
h.fwMap.Store(relPath, s)
return s
}
@@ -88,11 +91,10 @@ func (h *DirWatcher) Add(relPath string) Watcher {
func (h *DirWatcher) start() {
defer close(h.eventCh)
defer h.w.Close()
defer logrus.Debugf("directory watcher %s stopped", h.dir)
for {
select {
case <-h.ctx.Done():
case <-h.task.Context().Done():
return
case fsEvent, ok := <-h.w.Events:
if !ok {
@@ -122,9 +124,9 @@ func (h *DirWatcher) start() {
// send event to directory watcher
select {
case h.eventCh <- msg:
logrus.Debugf("sent event to directory watcher %s", h.dir)
h.Debug().Msg("sent event to directory watcher")
default:
logrus.Debugf("failed to send event to directory watcher %s", h.dir)
h.Debug().Msg("failed to send event to directory watcher")
}
// send event to file watcher too
@@ -132,12 +134,12 @@ func (h *DirWatcher) start() {
if ok {
select {
case w.eventCh <- msg:
logrus.Debugf("sent event to file watcher %s", relPath)
h.Debug().Msg("sent event to file watcher " + relPath)
default:
logrus.Debugf("failed to send event to file watcher %s", relPath)
h.Debug().Msg("failed to send event to file watcher " + relPath)
}
} else {
logrus.Debugf("file watcher not found: %s", relPath)
h.Debug().Msg("file watcher not found: " + relPath)
}
case err := <-h.w.Errors:
if errors.Is(err, fsnotify.ErrClosed) {

View File

@@ -2,12 +2,11 @@ package watcher
import (
"context"
"fmt"
"time"
docker_events "github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog"
D "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/watcher/events"
@@ -15,10 +14,11 @@ import (
type (
DockerWatcher struct {
zerolog.Logger
host string
client D.Client
clientOwned bool
logrus.FieldLogger
}
DockerListOptions = docker_events.ListOptions
)
@@ -47,7 +47,7 @@ var (
dockerWatcherRetryInterval = 3 * time.Second
)
func DockerrFilterContainer(nameOrID string) filters.KeyValuePair {
func DockerFilterContainerNameID(nameOrID string) filters.KeyValuePair {
return filters.Arg("container", nameOrID)
}
@@ -55,18 +55,21 @@ func NewDockerWatcher(host string) DockerWatcher {
return DockerWatcher{
host: host,
clientOwned: true,
FieldLogger: (logrus.
WithField("module", "docker_watcher").
WithField("host", host)),
Logger: logger.With().
Str("type", "docker").
Str("host", host).
Logger(),
}
}
func NewDockerWatcherWithClient(client D.Client) DockerWatcher {
return DockerWatcher{
client: client,
FieldLogger: (logrus.
WithField("module", "docker_watcher").
WithField("host", client.DaemonHost()))}
Logger: logger.With().
Str("type", "docker").
Str("host", client.DaemonHost()).
Logger(),
}
}
func (w DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Error) {
@@ -88,7 +91,7 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
}()
if !w.client.Connected() {
var err E.Error
var err error
attempts := 0
for {
w.client, err = D.ConnectClient(w.host)
@@ -96,7 +99,7 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
break
}
attempts++
errCh <- E.FailWith(fmt.Sprintf("docker connection attempt #%d", attempts), err)
errCh <- E.Errorf("docker connection attempt #%d: %w", attempts, err)
select {
case <-ctx.Done():
return
@@ -113,14 +116,14 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
for {
select {
case <-ctx.Done():
if err := E.From(ctx.Err()); err != nil && err.IsNot(context.Canceled) {
if err := E.From(ctx.Err()); err != nil && !err.Is(context.Canceled) {
errCh <- err
}
return
case msg := <-cEventCh:
action, ok := events.DockerEventMap[msg.Action]
if !ok {
w.Debugf("ignored unknown docker event: %s for container %s", msg.Action, msg.Actor.Attributes["name"])
w.Debug().Msgf("ignored unknown docker event: %s for container %s", msg.Action, msg.Actor.Attributes["name"])
continue
}
event := Event{

View File

@@ -65,7 +65,7 @@ func (e *EventQueue) Start(eventCh <-chan Event, errCh <-chan E.Error) {
go func() {
defer func() {
if err := recover(); err != nil {
e.onError(E.PanicRecv("onFlush: %s", err).Subject(e.task.Parent().Name()))
e.onError(E.Errorf("recovered panic in onFlush: %v", err).Subject(e.task.Parent().String()))
}
}()
e.onFlush(flushTask, queue)

View File

@@ -5,7 +5,7 @@ import (
"time"
"github.com/yusing/go-proxy/internal/net/types"
U "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
type JSONRepresentation struct {
@@ -27,10 +27,10 @@ func (jsonRepr *JSONRepresentation) MarshalJSON() ([]byte, error) {
"name": jsonRepr.Name,
"config": jsonRepr.Config,
"started": jsonRepr.Started.Unix(),
"startedStr": U.FormatTime(jsonRepr.Started),
"startedStr": strutils.FormatTime(jsonRepr.Started),
"status": jsonRepr.Status.String(),
"uptime": jsonRepr.Uptime.Seconds(),
"uptimeStr": U.FormatDuration(jsonRepr.Uptime),
"uptimeStr": strutils.FormatDuration(jsonRepr.Uptime),
"url": url,
"extra": jsonRepr.Extra,
})

View File

@@ -1,5 +1,7 @@
package health
import "github.com/sirupsen/logrus"
import (
"github.com/yusing/go-proxy/internal/logging"
)
var logger = logrus.WithField("module", "health_mon")
var logger = logging.With().Str("module", "health_mon").Logger()

View File

@@ -3,10 +3,14 @@ package health
import (
"context"
"errors"
"fmt"
"strings"
"time"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/notif"
"github.com/yusing/go-proxy/internal/task"
U "github.com/yusing/go-proxy/internal/utils"
F "github.com/yusing/go-proxy/internal/utils/functional"
@@ -29,6 +33,10 @@ type (
var monMap = F.NewMapOf[string, HealthMonitor]()
var (
ErrNegativeInterval = errors.New("negative interval")
)
func newMonitor(url types.URL, config *HealthCheckConfig, healthCheckFunc HealthCheckFunc) *monitor {
mon := &monitor{
config: config,
@@ -59,10 +67,12 @@ func (mon *monitor) Start(routeSubtask task.Task) E.Error {
mon.task = routeSubtask
if mon.config.Interval <= 0 {
return E.Invalid("interval", mon.config.Interval)
return E.From(ErrNegativeInterval)
}
go func() {
logger := logging.With().Str("name", mon.service).Logger()
defer func() {
if mon.status.Load() != StatusError {
mon.status.Store(StatusUnknown)
@@ -70,7 +80,7 @@ func (mon *monitor) Start(routeSubtask task.Task) E.Error {
}()
if err := mon.checkUpdateHealth(); err != nil {
logger.Errorf("healthchecker %s failure: %s", mon.service, err)
logger.Err(err).Msg("healthchecker failure")
return
}
@@ -87,7 +97,7 @@ func (mon *monitor) Start(routeSubtask task.Task) E.Error {
case <-ticker.C:
err := mon.checkUpdateHealth()
if err != nil {
logger.Errorf("healthchecker %s failure: %s", mon.service, err)
logger.Err(err).Msg("healthchecker failure")
return
}
}
@@ -128,10 +138,8 @@ func (mon *monitor) Uptime() time.Duration {
// Name implements HealthMonitor.
func (mon *monitor) Name() string {
if mon.task == nil {
return ""
}
return mon.task.Name()
parts := strings.Split(mon.service, "/")
return parts[len(parts)-1]
}
// String implements fmt.Stringer of HealthMonitor.
@@ -151,13 +159,14 @@ func (mon *monitor) MarshalJSON() ([]byte, error) {
}).MarshalJSON()
}
func (mon *monitor) checkUpdateHealth() E.Error {
func (mon *monitor) checkUpdateHealth() error {
logger := logging.With().Str("name", mon.Name()).Logger()
healthy, detail, err := mon.checkHealth()
if err != nil {
defer mon.task.Finish(err)
mon.status.Store(StatusError)
if !errors.Is(err, context.Canceled) {
return E.Failure("check health").With(err)
return fmt.Errorf("check health: %w", err)
}
return nil
}
@@ -169,9 +178,12 @@ func (mon *monitor) checkUpdateHealth() E.Error {
}
if healthy != (mon.status.Swap(status) == StatusHealthy) {
if healthy {
logger.Infof("%s is up", mon.service)
logger.Info().Msg("server is up")
notif.Notify(mon.service, "server is up")
} else {
logger.Warnf("%s is down: %s", mon.service, detail)
logger.Warn().Msg("server is down")
logger.Debug().Msg(detail)
notif.Notify(mon.service, "server is down")
}
}

View File

@@ -0,0 +1,5 @@
package watcher
import "github.com/yusing/go-proxy/internal/logging"
var logger = logging.With().Str("module", "watcher").Logger()