mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-19 20:32:35 +02:00
initial gotify support
This commit is contained in:
parent
a3ab32e9ab
commit
bee26f43d4
15 changed files with 415 additions and 38 deletions
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/config"
|
"github.com/yusing/go-proxy/internal/config"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||||
|
"github.com/yusing/go-proxy/internal/notif"
|
||||||
R "github.com/yusing/go-proxy/internal/route"
|
R "github.com/yusing/go-proxy/internal/route"
|
||||||
"github.com/yusing/go-proxy/internal/server"
|
"github.com/yusing/go-proxy/internal/server"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
@ -54,6 +55,7 @@ func main() {
|
||||||
TimestampFormat: timeFmt,
|
TimestampFormat: timeFmt,
|
||||||
})
|
})
|
||||||
logrus.Infof("go-proxy version %s", pkg.GetVersion())
|
logrus.Infof("go-proxy version %s", pkg.GetVersion())
|
||||||
|
logrus.AddHook(notif.GetDispatcher())
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.Command == common.CommandReload {
|
if args.Command == common.CommandReload {
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -8,6 +8,7 @@ require (
|
||||||
github.com/docker/docker v27.3.1+incompatible
|
github.com/docker/docker v27.3.1+incompatible
|
||||||
github.com/fsnotify/fsnotify v1.7.0
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
github.com/go-acme/lego/v4 v4.19.2
|
github.com/go-acme/lego/v4 v4.19.2
|
||||||
|
github.com/gotify/server/v2 v2.5.0
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
||||||
github.com/santhosh-tekuri/jsonschema v1.2.4
|
github.com/santhosh-tekuri/jsonschema v1.2.4
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
@ -40,7 +41,7 @@ require (
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.31.0 // indirect
|
go.opentelemetry.io/otel v1.31.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -49,6 +49,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gotify/server/v2 v2.5.0 h1:tJd+a5bb17X52f0EV2KxqLuyjQFKmVK1+t/iNUkP16Y=
|
||||||
|
github.com/gotify/server/v2 v2.5.0/go.mod h1:DKPMQI/FZ69iKbZvrOL6VWwRaoB9O+HDvJWVd/kiGbc=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||||
|
@ -84,8 +86,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
|
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
|
||||||
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
|
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/config/types"
|
"github.com/yusing/go-proxy/internal/config/types"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"github.com/yusing/go-proxy/internal/notif"
|
||||||
"github.com/yusing/go-proxy/internal/route"
|
"github.com/yusing/go-proxy/internal/route"
|
||||||
proxy "github.com/yusing/go-proxy/internal/route/provider"
|
proxy "github.com/yusing/go-proxy/internal/route/provider"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
@ -148,48 +149,59 @@ func (cfg *Config) StartProxyProviders() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) load() (res E.Error) {
|
func (cfg *Config) load() (res E.Error) {
|
||||||
b := E.NewBuilder("errors loading config")
|
errs := E.NewBuilder("errors loading config")
|
||||||
defer b.To(&res)
|
defer errs.To(&res)
|
||||||
|
|
||||||
logger.Debug("loading config")
|
logger.Debug("loading config")
|
||||||
defer logger.Debug("loaded config")
|
defer logger.Debug("loaded config")
|
||||||
|
|
||||||
data, err := E.Check(os.ReadFile(common.ConfigPath))
|
data, err := E.Check(os.ReadFile(common.ConfigPath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Add(E.FailWith("read config", err))
|
errs.Add(E.FailWith("read config", err))
|
||||||
logrus.Fatal(b.Build())
|
logrus.Fatal(errs.Build())
|
||||||
}
|
}
|
||||||
|
|
||||||
if !common.NoSchemaValidation {
|
if !common.NoSchemaValidation {
|
||||||
if err = Validate(data); err != nil {
|
if err = Validate(data); err != nil {
|
||||||
b.Add(E.FailWith("schema validation", err))
|
errs.Add(E.FailWith("schema validation", err))
|
||||||
logrus.Fatal(b.Build())
|
logrus.Fatal(errs.Build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model := types.DefaultConfig()
|
model := types.DefaultConfig()
|
||||||
if err := E.From(yaml.Unmarshal(data, model)); err != nil {
|
if err := E.From(yaml.Unmarshal(data, model)); err != nil {
|
||||||
b.Add(E.FailWith("parse config", err))
|
errs.Add(E.FailWith("parse config", err))
|
||||||
logrus.Fatal(b.Build())
|
logrus.Fatal(errs.Build())
|
||||||
}
|
}
|
||||||
|
|
||||||
// errors are non fatal below
|
// errors are non fatal below
|
||||||
b.Add(cfg.initAutoCert(&model.AutoCert))
|
errs.Add(cfg.initNotification(model.Providers.Notification))
|
||||||
b.Add(cfg.loadProviders(&model.Providers))
|
errs.Add(cfg.initAutoCert(&model.AutoCert))
|
||||||
|
errs.Add(cfg.loadRouteProviders(&model.Providers))
|
||||||
|
|
||||||
cfg.value = model
|
cfg.value = model
|
||||||
route.SetFindMuxDomains(model.MatchDomains)
|
route.SetFindMuxDomains(model.MatchDomains)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) initNotification(notifCfgMap types.NotificationConfigMap) (err E.Error) {
|
||||||
|
if len(notifCfgMap) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errs := E.NewBuilder("errors initializing notification providers")
|
||||||
|
|
||||||
|
for name, notifCfg := range notifCfgMap {
|
||||||
|
_, err := notif.RegisterProvider(cfg.task.Subtask(name), notifCfg)
|
||||||
|
errs.Add(err)
|
||||||
|
}
|
||||||
|
return errs.Build()
|
||||||
|
}
|
||||||
|
|
||||||
func (cfg *Config) initAutoCert(autocertCfg *types.AutoCertConfig) (err E.Error) {
|
func (cfg *Config) initAutoCert(autocertCfg *types.AutoCertConfig) (err E.Error) {
|
||||||
if cfg.autocertProvider != nil {
|
if cfg.autocertProvider != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug("initializing autocert")
|
|
||||||
defer logger.Debug("initialized autocert")
|
|
||||||
|
|
||||||
cfg.autocertProvider, err = autocert.NewConfig(autocertCfg).GetProvider()
|
cfg.autocertProvider, err = autocert.NewConfig(autocertCfg).GetProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = E.FailWith("autocert provider", err)
|
err = E.FailWith("autocert provider", err)
|
||||||
|
@ -197,11 +209,11 @@ func (cfg *Config) initAutoCert(autocertCfg *types.AutoCertConfig) (err E.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) loadProviders(providers *types.ProxyProviders) (outErr E.Error) {
|
func (cfg *Config) loadRouteProviders(providers *types.Providers) (outErr E.Error) {
|
||||||
subtask := cfg.task.Subtask("load providers")
|
subtask := cfg.task.Subtask("load route providers")
|
||||||
defer subtask.Finish("done")
|
defer subtask.Finish("done")
|
||||||
|
|
||||||
errs := E.NewBuilder("errors loading providers")
|
errs := E.NewBuilder("errors loading route providers")
|
||||||
results := E.NewBuilder("loaded providers")
|
results := E.NewBuilder("loaded providers")
|
||||||
defer errs.To(&outErr)
|
defer errs.To(&outErr)
|
||||||
|
|
||||||
|
|
|
@ -2,22 +2,23 @@ package types
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Config struct {
|
Config struct {
|
||||||
Providers ProxyProviders `json:"providers" yaml:",flow"`
|
Providers Providers `json:"providers" yaml:",flow"`
|
||||||
AutoCert AutoCertConfig `json:"autocert" yaml:",flow"`
|
AutoCert AutoCertConfig `json:"autocert" yaml:",flow"`
|
||||||
ExplicitOnly bool `json:"explicit_only" yaml:"explicit_only"`
|
ExplicitOnly bool `json:"explicit_only" yaml:"explicit_only"`
|
||||||
MatchDomains []string `json:"match_domains" yaml:"match_domains"`
|
MatchDomains []string `json:"match_domains" yaml:"match_domains"`
|
||||||
TimeoutShutdown int `json:"timeout_shutdown" yaml:"timeout_shutdown"`
|
TimeoutShutdown int `json:"timeout_shutdown" yaml:"timeout_shutdown"`
|
||||||
RedirectToHTTPS bool `json:"redirect_to_https" yaml:"redirect_to_https"`
|
RedirectToHTTPS bool `json:"redirect_to_https" yaml:"redirect_to_https"`
|
||||||
}
|
}
|
||||||
ProxyProviders struct {
|
Providers struct {
|
||||||
Files []string `json:"include" yaml:"include"` // docker, file
|
Files []string `json:"include" yaml:"include"`
|
||||||
Docker map[string]string `json:"docker" yaml:"docker"`
|
Docker map[string]string `json:"docker" yaml:"docker"`
|
||||||
|
Notification NotificationConfigMap `json:"notification" yaml:"notification"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Providers: ProxyProviders{},
|
Providers: Providers{},
|
||||||
TimeoutShutdown: 3,
|
TimeoutShutdown: 3,
|
||||||
RedirectToHTTPS: false,
|
RedirectToHTTPS: false,
|
||||||
}
|
}
|
||||||
|
|
5
internal/config/types/notif_config.go
Normal file
5
internal/config/types/notif_config.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import "github.com/yusing/go-proxy/internal/notif"
|
||||||
|
|
||||||
|
type NotificationConfigMap map[string]notif.ProviderConfig
|
|
@ -51,6 +51,16 @@ func FromJSON(data []byte) (Error, bool) {
|
||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TryUnwrap(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if unwrapped := errors.Unwrap(err); unwrapped != nil {
|
||||||
|
return unwrapped
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Check is a helper function that
|
// Check is a helper function that
|
||||||
// convert (T, error) to (T, NestedError).
|
// convert (T, error) to (T, NestedError).
|
||||||
func Check[T any](obj T, err error) (T, Error) {
|
func Check[T any](obj T, err error) (T, Error) {
|
||||||
|
@ -140,7 +150,8 @@ func (ne Error) With(s any) Error {
|
||||||
}
|
}
|
||||||
return ne.withError(ss)
|
return ne.withError(ss)
|
||||||
case error:
|
case error:
|
||||||
return ne.withError(From(ss))
|
// unwrap only once
|
||||||
|
return ne.withError(From(TryUnwrap(ss)))
|
||||||
case string:
|
case string:
|
||||||
msg = ss
|
msg = ss
|
||||||
case fmt.Stringer:
|
case fmt.Stringer:
|
||||||
|
@ -215,6 +226,13 @@ func (ne Error) HasError() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorf(format string, args ...any) Error {
|
func errorf(format string, args ...any) Error {
|
||||||
|
for i, arg := range args {
|
||||||
|
if err, ok := arg.(error); ok {
|
||||||
|
if unwrapped := errors.Unwrap(err); unwrapped != nil {
|
||||||
|
args[i] = unwrapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return From(fmt.Errorf(format, args...))
|
return From(fmt.Errorf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
100
internal/notif/dispatcher.go
Normal file
100
internal/notif/dispatcher.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package notif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Dispatcher struct {
|
||||||
|
task task.Task
|
||||||
|
logCh chan *logrus.Entry
|
||||||
|
providers F.Set[Provider]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var dispatcher *Dispatcher
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dispatcher = newNotifDispatcher()
|
||||||
|
go dispatcher.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNotifDispatcher() *Dispatcher {
|
||||||
|
return &Dispatcher{
|
||||||
|
task: task.GlobalTask("notif dispatcher"),
|
||||||
|
logCh: make(chan *logrus.Entry),
|
||||||
|
providers: F.NewSet[Provider](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDispatcher() *Dispatcher {
|
||||||
|
return dispatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterProvider(configSubTask task.Task, cfg ProviderConfig) (Provider, E.Error) {
|
||||||
|
name := configSubTask.Name()
|
||||||
|
createFunc, ok := Providers[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, E.NotExist("provider", name)
|
||||||
|
}
|
||||||
|
if provider, err := createFunc(cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
dispatcher.providers.Add(provider)
|
||||||
|
configSubTask.OnCancel("remove provider", func() {
|
||||||
|
dispatcher.providers.Remove(provider)
|
||||||
|
})
|
||||||
|
return provider, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (disp *Dispatcher) start() {
|
||||||
|
defer dispatcher.task.Finish("dispatcher stopped")
|
||||||
|
defer close(dispatcher.logCh)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-disp.task.Context().Done():
|
||||||
|
return
|
||||||
|
case entry := <-disp.logCh:
|
||||||
|
go disp.dispatch(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (disp *Dispatcher) dispatch(entry *logrus.Entry) {
|
||||||
|
task := disp.task.Subtask("dispatch notif")
|
||||||
|
defer task.Finish("notifs dispatched")
|
||||||
|
|
||||||
|
errs := E.NewBuilder("errors sending notif")
|
||||||
|
disp.providers.RangeAllParallel(func(p Provider) {
|
||||||
|
if err := p.Send(task.Context(), entry); err != nil {
|
||||||
|
errs.Addf("%s: %s", p.Name(), err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err := errs.Build(); err != nil {
|
||||||
|
logrus.Error("notif dispatcher failure: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Levels implements logrus.Hook.
|
||||||
|
func (disp *Dispatcher) Levels() []logrus.Level {
|
||||||
|
return []logrus.Level{
|
||||||
|
logrus.WarnLevel,
|
||||||
|
logrus.ErrorLevel,
|
||||||
|
logrus.FatalLevel,
|
||||||
|
logrus.PanicLevel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire implements logrus.Hook.
|
||||||
|
func (disp *Dispatcher) Fire(entry *logrus.Entry) error {
|
||||||
|
if disp.providers.Size() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
disp.logCh <- entry
|
||||||
|
return nil
|
||||||
|
}
|
111
internal/notif/gotify.go
Normal file
111
internal/notif/gotify.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
package notif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/gotify/server/v2/model"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
GotifyClient struct {
|
||||||
|
GotifyConfig
|
||||||
|
|
||||||
|
url *url.URL
|
||||||
|
http http.Client
|
||||||
|
}
|
||||||
|
GotifyConfig struct {
|
||||||
|
URL string `json:"url" yaml:"url"`
|
||||||
|
Token string `json:"token" yaml:"token"`
|
||||||
|
}
|
||||||
|
GotifyMessage model.Message
|
||||||
|
)
|
||||||
|
|
||||||
|
const gotifyMsgEndpoint = "/message"
|
||||||
|
|
||||||
|
func newGotifyClient(cfg map[string]any) (Provider, E.Error) {
|
||||||
|
client := new(GotifyClient)
|
||||||
|
err := U.Deserialize(cfg, &client.GotifyConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
url, uErr := url.Parse(client.URL)
|
||||||
|
if uErr != nil {
|
||||||
|
return nil, E.FailWith("parse url", uErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.url = url
|
||||||
|
return client, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements NotifProvider.
|
||||||
|
func (client *GotifyClient) Name() string {
|
||||||
|
return "gotify"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send implements NotifProvider.
|
||||||
|
func (client *GotifyClient) Send(ctx context.Context, entry *logrus.Entry) error {
|
||||||
|
var priority int
|
||||||
|
var title string
|
||||||
|
|
||||||
|
switch entry.Level {
|
||||||
|
case logrus.WarnLevel:
|
||||||
|
priority = 2
|
||||||
|
title = "Warning"
|
||||||
|
case logrus.ErrorLevel:
|
||||||
|
priority = 5
|
||||||
|
title = "Error"
|
||||||
|
case logrus.FatalLevel, logrus.PanicLevel:
|
||||||
|
priority = 8
|
||||||
|
title = "Critical"
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if subjects := FieldsAsTitle(entry); subjects != "" {
|
||||||
|
title = subjects + " " + title
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &GotifyMessage{
|
||||||
|
Title: title,
|
||||||
|
Message: entry.Message,
|
||||||
|
Priority: priority,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, client.url.String()+gotifyMsgEndpoint, bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+client.Token)
|
||||||
|
|
||||||
|
resp, err := client.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send gotify message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
var errm model.Error
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&errm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("gotify status %d, but failed to decode err response: %w", resp.StatusCode, err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("gotify status %d %s: %s", resp.StatusCode, errm.Error, errm.ErrorDescription)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
21
internal/notif/logrus.go
Normal file
21
internal/notif/logrus.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package notif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FieldsAsTitle(entry *logrus.Entry) string {
|
||||||
|
if len(entry.Data) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var parts []string
|
||||||
|
for k, v := range entry.Data {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s: %s", k, v))
|
||||||
|
}
|
||||||
|
parts[0] = U.Title(parts[0])
|
||||||
|
return strings.Join(parts, ", ")
|
||||||
|
}
|
21
internal/notif/providers.go
Normal file
21
internal/notif/providers.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package notif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Provider interface {
|
||||||
|
Name() string
|
||||||
|
Send(ctx context.Context, entry *logrus.Entry) error
|
||||||
|
}
|
||||||
|
ProviderCreateFunc func(map[string]any) (Provider, E.Error)
|
||||||
|
ProviderConfig map[string]any
|
||||||
|
)
|
||||||
|
|
||||||
|
var Providers = map[string]ProviderCreateFunc{
|
||||||
|
"gotify": newGotifyClient,
|
||||||
|
}
|
|
@ -9,10 +9,10 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
)
|
)
|
||||||
|
|
||||||
var globalTask = createGlobalTask()
|
var globalTask = createGlobalTask()
|
||||||
|
@ -21,7 +21,7 @@ func createGlobalTask() (t *task) {
|
||||||
t = new(task)
|
t = new(task)
|
||||||
t.name = "root"
|
t.name = "root"
|
||||||
t.ctx, t.cancel = context.WithCancelCause(context.Background())
|
t.ctx, t.cancel = context.WithCancelCause(context.Background())
|
||||||
t.subtasks = xsync.NewMapOf[*task, struct{}]()
|
t.subtasks = F.NewSet[*task]()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ type (
|
||||||
cancel context.CancelCauseFunc
|
cancel context.CancelCauseFunc
|
||||||
|
|
||||||
parent *task
|
parent *task
|
||||||
subtasks *xsync.MapOf[*task, struct{}]
|
subtasks F.Set[*task]
|
||||||
subTasksWg sync.WaitGroup
|
subTasksWg sync.WaitGroup
|
||||||
|
|
||||||
name, line string
|
name, line string
|
||||||
|
@ -209,7 +209,7 @@ func (t *task) OnFinished(about string, fn func()) {
|
||||||
defer t.OnFinishedMu.Unlock()
|
defer t.OnFinishedMu.Unlock()
|
||||||
|
|
||||||
if t.OnFinishedFuncs == nil {
|
if t.OnFinishedFuncs == nil {
|
||||||
onCompTask := GlobalTask(t.name + " > OnFinished")
|
onCompTask := GlobalTask(t.name + " > OnFinished > " + about)
|
||||||
go t.runAllOnFinished(onCompTask)
|
go t.runAllOnFinished(onCompTask)
|
||||||
}
|
}
|
||||||
var file string
|
var file string
|
||||||
|
@ -252,8 +252,8 @@ func (t *task) Finish(reason any) {
|
||||||
}
|
}
|
||||||
t.finishOnce.Do(func() {
|
t.finishOnce.Do(func() {
|
||||||
t.cancel(fmt.Errorf("%w: %s, reason: "+format, ErrTaskCanceled, t.name, reason))
|
t.cancel(fmt.Errorf("%w: %s, reason: "+format, ErrTaskCanceled, t.name, reason))
|
||||||
t.Wait()
|
|
||||||
})
|
})
|
||||||
|
t.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *task) Subtask(name string) Task {
|
func (t *task) Subtask(name string) Task {
|
||||||
|
@ -271,10 +271,10 @@ func (t *task) newSubTask(ctx context.Context, cancel context.CancelCauseFunc, n
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
name: name,
|
name: name,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
subtasks: xsync.NewMapOf[*task, struct{}](),
|
subtasks: F.NewSet[*task](),
|
||||||
}
|
}
|
||||||
parent.subTasksWg.Add(1)
|
parent.subTasksWg.Add(1)
|
||||||
parent.subtasks.Store(subtask, struct{}{})
|
parent.subtasks.Add(subtask)
|
||||||
if common.IsTrace {
|
if common.IsTrace {
|
||||||
_, file, line, ok := runtime.Caller(3)
|
_, file, line, ok := runtime.Caller(3)
|
||||||
if ok {
|
if ok {
|
||||||
|
@ -288,7 +288,7 @@ func (t *task) newSubTask(ctx context.Context, cancel context.CancelCauseFunc, n
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
subtask.Wait()
|
subtask.Wait()
|
||||||
parent.subtasks.Delete(subtask)
|
parent.subtasks.Remove(subtask)
|
||||||
parent.subTasksWg.Done()
|
parent.subTasksWg.Done()
|
||||||
}()
|
}()
|
||||||
return subtask
|
return subtask
|
||||||
|
@ -331,9 +331,8 @@ func (t *task) tree(prefix ...string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sb.WriteString(t.Name() + "\n")
|
sb.WriteString(t.Name() + "\n")
|
||||||
t.subtasks.Range(func(subtask *task, _ struct{}) bool {
|
t.subtasks.RangeAll(func(subtask *task) {
|
||||||
sb.WriteString(subtask.tree(pre + " "))
|
sb.WriteString(subtask.tree(pre + " "))
|
||||||
return true
|
|
||||||
})
|
})
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
@ -362,9 +361,8 @@ func (t *task) serialize() map[string]any {
|
||||||
}
|
}
|
||||||
if t.subtasks.Size() > 0 {
|
if t.subtasks.Size() > 0 {
|
||||||
m["subtasks"] = make([]map[string]any, 0, t.subtasks.Size())
|
m["subtasks"] = make([]map[string]any, 0, t.subtasks.Size())
|
||||||
t.subtasks.Range(func(subtask *task, _ struct{}) bool {
|
t.subtasks.RangeAll(func(subtask *task) {
|
||||||
m["subtasks"] = append(m["subtasks"].([]map[string]any), subtask.serialize())
|
m["subtasks"] = append(m["subtasks"].([]map[string]any), subtask.serialize())
|
||||||
return true
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return m
|
return m
|
||||||
|
|
|
@ -114,9 +114,9 @@ func (m Map[KT, VT]) RangeAll(do func(k KT, v VT)) {
|
||||||
// nothing
|
// nothing
|
||||||
func (m Map[KT, VT]) RangeAllParallel(do func(k KT, v VT)) {
|
func (m Map[KT, VT]) RangeAllParallel(do func(k KT, v VT)) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(m.Size())
|
|
||||||
|
|
||||||
m.Range(func(k KT, v VT) bool {
|
m.Range(func(k KT, v VT) bool {
|
||||||
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
do(k, v)
|
do(k, v)
|
||||||
wg.Done()
|
wg.Done()
|
||||||
|
|
59
internal/utils/functional/set.go
Normal file
59
internal/utils/functional/set.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package functional
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Set[T comparable] struct {
|
||||||
|
m *xsync.MapOf[T, struct{}]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSet[T comparable]() Set[T] {
|
||||||
|
return Set[T]{m: xsync.NewMapOf[T, struct{}]()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (set Set[T]) Add(v T) {
|
||||||
|
set.m.Store(v, struct{}{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (set Set[T]) Remove(v T) {
|
||||||
|
set.m.Delete(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (set Set[T]) Contains(v T) bool {
|
||||||
|
_, ok := set.m.Load(v)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (set Set[T]) Range(f func(T) bool) {
|
||||||
|
set.m.Range(func(k T, _ struct{}) bool {
|
||||||
|
return f(k)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (set Set[T]) RangeAll(f func(T)) {
|
||||||
|
set.m.Range(func(k T, _ struct{}) bool {
|
||||||
|
f(k)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (set Set[T]) RangeAllParallel(f func(T)) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
set.Range(func(k T) bool {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
f(k)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (set Set[T]) Size() int {
|
||||||
|
return set.m.Size()
|
||||||
|
}
|
|
@ -294,6 +294,32 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"description": "Notification provider configuration",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"gotify": {
|
||||||
|
"description": "Gotify configuration",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"description": "Gotify URL",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"description": "Gotify token",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"url",
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue