Logging
This commit is contained in:
		
							parent
							
								
									bd865fd55d
								
							
						
					
					
						commit
						ab955d4d1c
					
				
					 15 changed files with 161 additions and 65 deletions
				
			
		|  | @ -28,7 +28,7 @@ var cmdAccess = &cli.Command{ | ||||||
| 	Usage:     "Grant/revoke access to a topic, or show access", | 	Usage:     "Grant/revoke access to a topic, or show access", | ||||||
| 	UsageText: "ntfy access [USERNAME [TOPIC [PERMISSION]]]", | 	UsageText: "ntfy access [USERNAME [TOPIC [PERMISSION]]]", | ||||||
| 	Flags:     flagsAccess, | 	Flags:     flagsAccess, | ||||||
| 	Before:    initLogFunc(initConfigFileInputSourceFunc("config", flagsAccess)), | 	Before:    initConfigFileInputSourceFunc("config", flagsAccess, initLogFunc), | ||||||
| 	Action:    execUserAccess, | 	Action:    execUserAccess, | ||||||
| 	Category:  categoryServer, | 	Category:  categoryServer, | ||||||
| 	Description: `Manage the access control list for the ntfy server. | 	Description: `Manage the access control list for the ntfy server. | ||||||
|  |  | ||||||
							
								
								
									
										14
									
								
								cmd/app.go
									
										
									
									
									
								
							
							
						
						
									
										14
									
								
								cmd/app.go
									
										
									
									
									
								
							|  | @ -3,6 +3,7 @@ package cmd | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"github.com/urfave/cli/v2" | 	"github.com/urfave/cli/v2" | ||||||
|  | 	"github.com/urfave/cli/v2/altsrc" | ||||||
| 	"heckel.io/ntfy/log" | 	"heckel.io/ntfy/log" | ||||||
| 	"os" | 	"os" | ||||||
| ) | ) | ||||||
|  | @ -16,7 +17,7 @@ var commands = make([]*cli.Command, 0) | ||||||
| 
 | 
 | ||||||
| var flagsDefault = []cli.Flag{ | var flagsDefault = []cli.Flag{ | ||||||
| 	&cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, EnvVars: []string{"NTFY_DEBUG"}, Usage: "enable debug logging"}, | 	&cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, EnvVars: []string{"NTFY_DEBUG"}, Usage: "enable debug logging"}, | ||||||
| 	&cli.StringFlag{Name: "log-level", Aliases: []string{"log_level"}, Value: log.InfoLevel.String(), EnvVars: []string{"NTFY_LOG_LEVEL"}, Usage: "set log level"}, | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "log-level", Aliases: []string{"log_level"}, Value: log.InfoLevel.String(), EnvVars: []string{"NTFY_LOG_LEVEL"}, Usage: "set log level"}), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // New creates a new CLI application | // New creates a new CLI application | ||||||
|  | @ -32,22 +33,15 @@ func New() *cli.App { | ||||||
| 		ErrWriter:              os.Stderr, | 		ErrWriter:              os.Stderr, | ||||||
| 		Commands:               commands, | 		Commands:               commands, | ||||||
| 		Flags:                  flagsDefault, | 		Flags:                  flagsDefault, | ||||||
| 		Before:                 initLogFunc(nil), | 		Before:                 initLogFunc, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func initLogFunc(next cli.BeforeFunc) cli.BeforeFunc { | func initLogFunc(c *cli.Context) error { | ||||||
| 	return func(c *cli.Context) error { |  | ||||||
| 	if c.Bool("debug") { | 	if c.Bool("debug") { | ||||||
| 		log.SetLevel(log.DebugLevel) | 		log.SetLevel(log.DebugLevel) | ||||||
| 	} else { | 	} else { | ||||||
| 		log.SetLevel(log.ToLevel(c.String("log-level"))) | 		log.SetLevel(log.ToLevel(c.String("log-level"))) | ||||||
| 	} | 	} | ||||||
| 		if next != nil { |  | ||||||
| 			if err := next(c); err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	return nil | 	return nil | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| // initConfigFileInputSourceFunc is like altsrc.InitInputSourceWithContext and altsrc.NewYamlSourceFromFlagFunc, but checks | // initConfigFileInputSourceFunc is like altsrc.InitInputSourceWithContext and altsrc.NewYamlSourceFromFlagFunc, but checks | ||||||
| // if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails. | // if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails. | ||||||
| func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag) cli.BeforeFunc { | func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag, next cli.BeforeFunc) cli.BeforeFunc { | ||||||
| 	return func(context *cli.Context) error { | 	return func(context *cli.Context) error { | ||||||
| 		configFile := context.String(configFlag) | 		configFile := context.String(configFlag) | ||||||
| 		if context.IsSet(configFlag) && !util.FileExists(configFile) { | 		if context.IsSet(configFlag) && !util.FileExists(configFile) { | ||||||
|  | @ -23,7 +23,15 @@ func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag) cli.Befo | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		return altsrc.ApplyInputSourceValues(context, inputSource, flags) | 		if err := altsrc.ApplyInputSourceValues(context, inputSource, flags); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		if next != nil { | ||||||
|  | 			if err := next(context); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -44,7 +44,7 @@ var cmdPublish = &cli.Command{ | ||||||
| 	Action:    execPublish, | 	Action:    execPublish, | ||||||
| 	Category:  categoryClient, | 	Category:  categoryClient, | ||||||
| 	Flags:     flagsPublish, | 	Flags:     flagsPublish, | ||||||
| 	Before:    initLogFunc(nil), | 	Before:    initLogFunc, | ||||||
| 	Description: `Publish a message to a ntfy server. | 	Description: `Publish a message to a ntfy server. | ||||||
| 
 | 
 | ||||||
| Examples: | Examples: | ||||||
|  |  | ||||||
							
								
								
									
										45
									
								
								cmd/serve.go
									
										
									
									
									
								
							
							
						
						
									
										45
									
								
								cmd/serve.go
									
										
									
									
									
								
							|  | @ -8,7 +8,10 @@ import ( | ||||||
| 	"heckel.io/ntfy/log" | 	"heckel.io/ntfy/log" | ||||||
| 	"math" | 	"math" | ||||||
| 	"net" | 	"net" | ||||||
|  | 	"os" | ||||||
|  | 	"os/signal" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"syscall" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/urfave/cli/v2" | 	"github.com/urfave/cli/v2" | ||||||
|  | @ -21,9 +24,13 @@ func init() { | ||||||
| 	commands = append(commands, cmdServe) | 	commands = append(commands, cmdServe) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | const ( | ||||||
|  | 	defaultServerConfigFile = "/etc/ntfy/server.yml" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| var flagsServe = append( | var flagsServe = append( | ||||||
| 	flagsDefault, | 	flagsDefault, | ||||||
| 	&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"}, | 	&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"}, | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"base_url", "B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"base_url", "B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}), | ||||||
|  | @ -69,7 +76,7 @@ var cmdServe = &cli.Command{ | ||||||
| 	Action:    execServe, | 	Action:    execServe, | ||||||
| 	Category:  categoryServer, | 	Category:  categoryServer, | ||||||
| 	Flags:     flagsServe, | 	Flags:     flagsServe, | ||||||
| 	Before:    initLogFunc(initConfigFileInputSourceFunc("config", flagsServe)), | 	Before:    initConfigFileInputSourceFunc("config", flagsServe, initLogFunc), | ||||||
| 	Description: `Run the ntfy server and listen for incoming requests | 	Description: `Run the ntfy server and listen for incoming requests | ||||||
| 
 | 
 | ||||||
| The command will load the configuration from /etc/ntfy/server.yml. Config options can  | The command will load the configuration from /etc/ntfy/server.yml. Config options can  | ||||||
|  | @ -86,6 +93,7 @@ func execServe(c *cli.Context) error { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Read all the options | 	// Read all the options | ||||||
|  | 	config := c.String("config") | ||||||
| 	baseURL := c.String("base-url") | 	baseURL := c.String("base-url") | ||||||
| 	listenHTTP := c.String("listen-http") | 	listenHTTP := c.String("listen-http") | ||||||
| 	listenHTTPS := c.String("listen-https") | 	listenHTTPS := c.String("listen-https") | ||||||
|  | @ -241,11 +249,15 @@ func execServe(c *cli.Context) error { | ||||||
| 	conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish | 	conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish | ||||||
| 	conf.BehindProxy = behindProxy | 	conf.BehindProxy = behindProxy | ||||||
| 	conf.EnableWeb = enableWeb | 	conf.EnableWeb = enableWeb | ||||||
|  | 
 | ||||||
|  | 	// Set up hot-reloading of config | ||||||
|  | 	go sigHandlerConfigReload(config) | ||||||
|  | 
 | ||||||
|  | 	// Run server | ||||||
| 	s, err := server.New(conf) | 	s, err := server.New(conf) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} else if err := s.Run(); err != nil { | ||||||
| 	if err := s.Run(); err != nil { |  | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	log.Info("Exiting.") | 	log.Info("Exiting.") | ||||||
|  | @ -262,3 +274,28 @@ func parseSize(s string, defaultValue int64) (v int64, err error) { | ||||||
| 	} | 	} | ||||||
| 	return v, nil | 	return v, nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func sigHandlerConfigReload(config string) { | ||||||
|  | 	sigs := make(chan os.Signal, 1) | ||||||
|  | 	signal.Notify(sigs, syscall.SIGHUP) | ||||||
|  | 	for range sigs { | ||||||
|  | 		log.Info("Partially hot reloading configuration ...") | ||||||
|  | 		inputSource, err := newYamlSourceFromFile(config, flagsServe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Warn("Hot reload failed: %s", err.Error()) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		reloadLogLevel(inputSource) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func reloadLogLevel(inputSource altsrc.InputSourceContext) { | ||||||
|  | 	newLevelStr, err := inputSource.String("log-level") | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Warn("Cannot load log level: %s", err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	newLevel := log.ToLevel(newLevelStr) | ||||||
|  | 	log.SetLevel(newLevel) | ||||||
|  | 	log.Info("Log level is %s", newLevel.String()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -43,7 +43,7 @@ var cmdSubscribe = &cli.Command{ | ||||||
| 	Action:    execSubscribe, | 	Action:    execSubscribe, | ||||||
| 	Category:  categoryClient, | 	Category:  categoryClient, | ||||||
| 	Flags:     flagsSubscribe, | 	Flags:     flagsSubscribe, | ||||||
| 	Before:    initLogFunc(nil), | 	Before:    initLogFunc, | ||||||
| 	Description: `Subscribe to a topic from a ntfy server, and either print or execute a command for  | 	Description: `Subscribe to a topic from a ntfy server, and either print or execute a command for  | ||||||
| every arriving message. There are 3 modes in which the command can be run: | every arriving message. There are 3 modes in which the command can be run: | ||||||
| 
 | 
 | ||||||
|  | @ -253,7 +253,7 @@ func loadConfig(c *cli.Context) (*client.Config, error) { | ||||||
| 	if filename != "" { | 	if filename != "" { | ||||||
| 		return client.LoadConfig(filename) | 		return client.LoadConfig(filename) | ||||||
| 	} | 	} | ||||||
| 	configFile := defaultConfigFile() | 	configFile := defaultClientConfigFile() | ||||||
| 	if s, _ := os.Stat(configFile); s != nil { | 	if s, _ := os.Stat(configFile); s != nil { | ||||||
| 		return client.LoadConfig(configFile) | 		return client.LoadConfig(configFile) | ||||||
| 	} | 	} | ||||||
|  | @ -261,7 +261,7 @@ func loadConfig(c *cli.Context) (*client.Config, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| //lint:ignore U1000 Conditionally used in different builds | //lint:ignore U1000 Conditionally used in different builds | ||||||
| func defaultConfigFileUnix() string { | func defaultClientConfigFileUnix() string { | ||||||
| 	u, _ := user.Current() | 	u, _ := user.Current() | ||||||
| 	configFile := clientRootConfigFileUnixAbsolute | 	configFile := clientRootConfigFileUnixAbsolute | ||||||
| 	if u.Uid != "0" { | 	if u.Uid != "0" { | ||||||
|  | @ -272,7 +272,7 @@ func defaultConfigFileUnix() string { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| //lint:ignore U1000 Conditionally used in different builds | //lint:ignore U1000 Conditionally used in different builds | ||||||
| func defaultConfigFileWindows() string { | func defaultClientConfigFileWindows() string { | ||||||
| 	homeDir, _ := os.UserConfigDir() | 	homeDir, _ := os.UserConfigDir() | ||||||
| 	return filepath.Join(homeDir, clientUserConfigFileWindowsRelative) | 	return filepath.Join(homeDir, clientUserConfigFileWindowsRelative) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -11,6 +11,6 @@ var ( | ||||||
| 	scriptLauncher = []string{"sh", "-c"} | 	scriptLauncher = []string{"sh", "-c"} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func defaultConfigFile() string { | func defaultClientConfigFile() string { | ||||||
| 	return defaultConfigFileUnix() | 	return defaultClientConfigFileUnix() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -11,6 +11,6 @@ var ( | ||||||
| 	scriptLauncher = []string{"sh", "-c"} | 	scriptLauncher = []string{"sh", "-c"} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func defaultConfigFile() string { | func defaultClientConfigFile() string { | ||||||
| 	return defaultConfigFileUnix() | 	return defaultClientConfigFileUnix() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -11,5 +11,5 @@ var ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func defaultConfigFile() string { | func defaultConfigFile() string { | ||||||
| 	return defaultConfigFileWindows() | 	return defaultClientConfigFileWindows() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -29,7 +29,7 @@ var cmdUser = &cli.Command{ | ||||||
| 	Usage:     "Manage/show users", | 	Usage:     "Manage/show users", | ||||||
| 	UsageText: "ntfy user [list|add|remove|change-pass|change-role] ...", | 	UsageText: "ntfy user [list|add|remove|change-pass|change-role] ...", | ||||||
| 	Flags:     flagsUser, | 	Flags:     flagsUser, | ||||||
| 	Before:    initLogFunc(initConfigFileInputSourceFunc("config", flagsUser)), | 	Before:    initConfigFileInputSourceFunc("config", flagsUser, initLogFunc), | ||||||
| 	Category:  categoryServer, | 	Category:  categoryServer, | ||||||
| 	Subcommands: []*cli.Command{ | 	Subcommands: []*cli.Command{ | ||||||
| 		{ | 		{ | ||||||
|  |  | ||||||
							
								
								
									
										26
									
								
								log/log.go
									
										
									
									
									
								
							
							
						
						
									
										26
									
								
								log/log.go
									
										
									
									
									
								
							|  | @ -3,10 +3,13 @@ package log | ||||||
| import ( | import ( | ||||||
| 	"log" | 	"log" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"sync" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // Level is a well-known log level, as defined below | ||||||
| type Level int | type Level int | ||||||
| 
 | 
 | ||||||
|  | // Well known log levels | ||||||
| const ( | const ( | ||||||
| 	DebugLevel Level = iota | 	DebugLevel Level = iota | ||||||
| 	InfoLevel | 	InfoLevel | ||||||
|  | @ -30,32 +33,50 @@ func (l Level) String() string { | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	level = InfoLevel | 	level = InfoLevel | ||||||
|  | 	mu    = &sync.Mutex{} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // Debug prints the given message, if the current log level is DEBUG | ||||||
| func Debug(message string, v ...interface{}) { | func Debug(message string, v ...interface{}) { | ||||||
| 	logIf(DebugLevel, message, v...) | 	logIf(DebugLevel, message, v...) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Info prints the given message, if the current log level is INFO or lower | ||||||
| func Info(message string, v ...interface{}) { | func Info(message string, v ...interface{}) { | ||||||
| 	logIf(InfoLevel, message, v...) | 	logIf(InfoLevel, message, v...) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Warn prints the given message, if the current log level is WARN or lower | ||||||
| func Warn(message string, v ...interface{}) { | func Warn(message string, v ...interface{}) { | ||||||
| 	logIf(WarnLevel, message, v...) | 	logIf(WarnLevel, message, v...) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Error prints the given message, if the current log level is ERROR or lower | ||||||
| func Error(message string, v ...interface{}) { | func Error(message string, v ...interface{}) { | ||||||
| 	logIf(ErrorLevel, message, v...) | 	logIf(ErrorLevel, message, v...) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Fatal prints the given message, and exits the program | ||||||
| func Fatal(v ...interface{}) { | func Fatal(v ...interface{}) { | ||||||
| 	log.Fatalln(v...) | 	log.Fatalln(v...) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // CurrentLevel returns the current log level | ||||||
|  | func CurrentLevel() Level { | ||||||
|  | 	mu.Lock() | ||||||
|  | 	defer mu.Unlock() | ||||||
|  | 	return level | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetLevel sets a new log level | ||||||
| func SetLevel(newLevel Level) { | func SetLevel(newLevel Level) { | ||||||
|  | 	mu.Lock() | ||||||
|  | 	defer mu.Unlock() | ||||||
| 	level = newLevel | 	level = newLevel | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // ToLevel converts a string to a Level. It returns InfoLevel if the string | ||||||
|  | // does not match any known log levels. | ||||||
| func ToLevel(s string) Level { | func ToLevel(s string) Level { | ||||||
| 	switch strings.ToLower(s) { | 	switch strings.ToLower(s) { | ||||||
| 	case "debug": | 	case "debug": | ||||||
|  | @ -67,13 +88,12 @@ func ToLevel(s string) Level { | ||||||
| 	case "error": | 	case "error": | ||||||
| 		return ErrorLevel | 		return ErrorLevel | ||||||
| 	default: | 	default: | ||||||
| 		log.Fatalf("unknown log level: %s", s) | 		return InfoLevel | ||||||
| 		return 0 |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func logIf(l Level, message string, v ...interface{}) { | func logIf(l Level, message string, v ...interface{}) { | ||||||
| 	if level <= l { | 	if CurrentLevel() <= l { | ||||||
| 		log.Printf(l.String()+" "+message, v...) | 		log.Printf(l.String()+" "+message, v...) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ After=network.target | ||||||
| User=ntfy | User=ntfy | ||||||
| Group=ntfy | Group=ntfy | ||||||
| ExecStart=/usr/bin/ntfy serve | ExecStart=/usr/bin/ntfy serve | ||||||
|  | ExecReload=/bin/kill --signal HUP $MAINPID | ||||||
| Restart=on-failure | Restart=on-failure | ||||||
| AmbientCapabilities=CAP_NET_BIND_SERVICE | AmbientCapabilities=CAP_NET_BIND_SERVICE | ||||||
| LimitNOFILE=10000 | LimitNOFILE=10000 | ||||||
|  |  | ||||||
|  | @ -179,7 +179,7 @@ func (s *Server) Run() error { | ||||||
| 	if s.config.SMTPServerListen != "" { | 	if s.config.SMTPServerListen != "" { | ||||||
| 		listenStr += fmt.Sprintf(" %s[smtp]", s.config.SMTPServerListen) | 		listenStr += fmt.Sprintf(" %s[smtp]", s.config.SMTPServerListen) | ||||||
| 	} | 	} | ||||||
| 	log.Info("Listening on%s", listenStr) | 	log.Info("Listening on%s, log level is %s", listenStr, log.CurrentLevel().String()) | ||||||
| 	mux := http.NewServeMux() | 	mux := http.NewServeMux() | ||||||
| 	mux.HandleFunc("/", s.handle) | 	mux.HandleFunc("/", s.handle) | ||||||
| 	errChan := make(chan error) | 	errChan := make(chan error) | ||||||
|  | @ -246,18 +246,28 @@ func (s *Server) Stop() { | ||||||
| 
 | 
 | ||||||
| func (s *Server) handle(w http.ResponseWriter, r *http.Request) { | func (s *Server) handle(w http.ResponseWriter, r *http.Request) { | ||||||
| 	v := s.visitor(r) | 	v := s.visitor(r) | ||||||
| 	log.Debug("[%s] %s %s", v.ip, r.Method, r.URL.Path) | 	log.Debug("%s HTTP %s %s", v.ip, r.Method, r.URL.Path) | ||||||
| 
 |  | ||||||
| 	if err := s.handleInternal(w, r, v); err != nil { | 	if err := s.handleInternal(w, r, v); err != nil { | ||||||
| 		if websocket.IsWebSocketUpgrade(r) { | 		if websocket.IsWebSocketUpgrade(r) { | ||||||
| 			log.Info("[%s] WS %s %s - %s", v.ip, r.Method, r.URL.Path, err.Error()) | 			isNormalError := websocket.IsCloseError(err, websocket.CloseAbnormalClosure) || strings.Contains(err.Error(), "i/o timeout") | ||||||
|  | 			if isNormalError { | ||||||
|  | 				log.Debug("%s WS %s %s - %s", v.ip, r.Method, r.URL.Path, err.Error()) | ||||||
|  | 			} else { | ||||||
|  | 				log.Warn("%s WS %s %s - %s", v.ip, r.Method, r.URL.Path, err.Error()) | ||||||
|  | 			} | ||||||
| 			return // Do not attempt to write to upgraded connection | 			return // Do not attempt to write to upgraded connection | ||||||
| 		} | 		} | ||||||
| 		httpErr, ok := err.(*errHTTP) | 		httpErr, ok := err.(*errHTTP) | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			httpErr = errHTTPInternalError | 			httpErr = errHTTPInternalError | ||||||
| 		} | 		} | ||||||
| 		log.Info("[%s] HTTP %s %s - %d - %d - %s", v.ip, r.Method, r.URL.Path, httpErr.HTTPCode, httpErr.Code, err.Error()) | 		isNormalError := httpErr.Code == 404 | ||||||
|  | 		if isNormalError { | ||||||
|  | 			log.Debug("%s HTTP %s %s - %d - %d - %s", v.ip, r.Method, r.URL.Path, httpErr.HTTPCode, httpErr.Code, err.Error()) | ||||||
|  | 		} else { | ||||||
|  | 			log.Info("%s HTTP %s %s - %d - %d - %s", v.ip, r.Method, r.URL.Path, httpErr.HTTPCode, httpErr.Code, err.Error()) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		w.Header().Set("Content-Type", "application/json") | 		w.Header().Set("Content-Type", "application/json") | ||||||
| 		w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests | 		w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests | ||||||
| 		w.WriteHeader(httpErr.HTTPCode) | 		w.WriteHeader(httpErr.HTTPCode) | ||||||
|  | @ -434,22 +444,24 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito | ||||||
| 		m.Message = emptyMessageBody | 		m.Message = emptyMessageBody | ||||||
| 	} | 	} | ||||||
| 	delayed := m.Time > time.Now().Unix() | 	delayed := m.Time > time.Now().Unix() | ||||||
| 	log.Debug("[%s] %s %s: ev=%s, body=%d bytes, delayed=%t, fb=%t, cache=%t, up=%t, email=%s", | 	log.Debug("%s Received message: ev=%s, body=%d bytes, delayed=%t, fb=%t, cache=%t, up=%t, email=%s", | ||||||
| 		v.ip, r.Method, r.URL.Path, m.Event, len(body.PeekedBytes), delayed, firebase, cache, unifiedpush, email) | 		logPrefix(v, m), m.Event, len(body.PeekedBytes), delayed, firebase, cache, unifiedpush, email) | ||||||
| 	if !delayed { | 	if !delayed { | ||||||
| 		if err := t.Publish(v, m); err != nil { | 		if err := t.Publish(v, m); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 		if s.firebaseClient != nil && firebase { | ||||||
| 	if s.firebaseClient != nil && firebase && !delayed { |  | ||||||
| 			go s.sendToFirebase(v, m) | 			go s.sendToFirebase(v, m) | ||||||
| 		} | 		} | ||||||
| 	if s.mailer != nil && email != "" && !delayed { | 		if s.mailer != nil && email != "" { | ||||||
| 			go s.sendEmail(v, m, email) | 			go s.sendEmail(v, m, email) | ||||||
| 		} | 		} | ||||||
| 	if s.config.UpstreamBaseURL != "" && !delayed { | 		if s.config.UpstreamBaseURL != "" { | ||||||
| 			go s.forwardPollRequest(v, m) | 			go s.forwardPollRequest(v, m) | ||||||
| 		} | 		} | ||||||
|  | 	} else { | ||||||
|  | 		log.Debug("%s Message delayed, will process later", logPrefix(v, m)) | ||||||
|  | 	} | ||||||
| 	if cache { | 	if cache { | ||||||
| 		if err := s.messageCache.AddMessage(m); err != nil { | 		if err := s.messageCache.AddMessage(m); err != nil { | ||||||
| 			return err | 			return err | ||||||
|  | @ -467,14 +479,16 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *Server) sendToFirebase(v *visitor, m *message) { | func (s *Server) sendToFirebase(v *visitor, m *message) { | ||||||
|  | 	log.Debug("%s Publishing to Firebase", logPrefix(v, m)) | ||||||
| 	if err := s.firebaseClient.Send(v, m); err != nil { | 	if err := s.firebaseClient.Send(v, m); err != nil { | ||||||
| 		log.Warn("[%s] FB - Unable to publish to Firebase: %v", v.ip, err.Error()) | 		log.Warn("%s Unable to publish to Firebase: %v", logPrefix(v, m), err.Error()) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *Server) sendEmail(v *visitor, m *message, email string) { | func (s *Server) sendEmail(v *visitor, m *message, email string) { | ||||||
|  | 	log.Debug("%s Sending email to %s", logPrefix(v, m), email) | ||||||
| 	if err := s.mailer.Send(v.ip, email, m); err != nil { | 	if err := s.mailer.Send(v.ip, email, m); err != nil { | ||||||
| 		log.Warn("[%s] MAIL - Unable to send email: %v", v.ip, err.Error()) | 		log.Warn("%s Unable to send email: %v", logPrefix(v, m), err.Error()) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -482,9 +496,10 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { | ||||||
| 	topicURL := fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic) | 	topicURL := fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic) | ||||||
| 	topicHash := fmt.Sprintf("%x", sha256.Sum256([]byte(topicURL))) | 	topicHash := fmt.Sprintf("%x", sha256.Sum256([]byte(topicURL))) | ||||||
| 	forwardURL := fmt.Sprintf("%s/%s", s.config.UpstreamBaseURL, topicHash) | 	forwardURL := fmt.Sprintf("%s/%s", s.config.UpstreamBaseURL, topicHash) | ||||||
|  | 	log.Debug("%s Publishing poll request to %s", logPrefix(v, m), forwardURL) | ||||||
| 	req, err := http.NewRequest("POST", forwardURL, strings.NewReader("")) | 	req, err := http.NewRequest("POST", forwardURL, strings.NewReader("")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Warn("[%s] FWD - Unable to forward poll request: %v", v.ip, err.Error()) | 		log.Warn("%s Unable to publish poll request: %v", logPrefix(v, m), err.Error()) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	req.Header.Set("X-Poll-ID", m.ID) | 	req.Header.Set("X-Poll-ID", m.ID) | ||||||
|  | @ -493,10 +508,10 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { | ||||||
| 	} | 	} | ||||||
| 	response, err := httpClient.Do(req) | 	response, err := httpClient.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Warn("[%s] FWD - Unable to forward poll request: %v", v.ip, err.Error()) | 		log.Warn("%s Unable to publish poll request: %v", logPrefix(v, m), err.Error()) | ||||||
| 		return | 		return | ||||||
| 	} else if response.StatusCode != http.StatusOK { | 	} else if response.StatusCode != http.StatusOK { | ||||||
| 		log.Warn("[%s] FWD - Unable to forward poll request, unexpected status: %d", v.ip, response.StatusCode) | 		log.Warn("%s Unable to publish poll request, unexpected HTTP status: %d", logPrefix(v, m), response.StatusCode) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -1012,6 +1027,7 @@ func (s *Server) updateStatsAndPrune() { | ||||||
| 	// Expire visitors from rate visitors map | 	// Expire visitors from rate visitors map | ||||||
| 	for ip, v := range s.visitors { | 	for ip, v := range s.visitors { | ||||||
| 		if v.Stale() { | 		if v.Stale() { | ||||||
|  | 			log.Debug("Deleting stale visitor %s", v.ip) | ||||||
| 			delete(s.visitors, ip) | 			delete(s.visitors, ip) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -1019,17 +1035,21 @@ func (s *Server) updateStatsAndPrune() { | ||||||
| 	// Delete expired attachments | 	// Delete expired attachments | ||||||
| 	if s.fileCache != nil { | 	if s.fileCache != nil { | ||||||
| 		ids, err := s.messageCache.AttachmentsExpired() | 		ids, err := s.messageCache.AttachmentsExpired() | ||||||
| 		if err == nil { | 		if err != nil { | ||||||
|  | 			log.Warn("Error retrieving expired attachments: %s", err.Error()) | ||||||
|  | 		} else if len(ids) > 0 { | ||||||
|  | 			log.Debug("Deleting expired attachments: %v", ids) | ||||||
| 			if err := s.fileCache.Remove(ids...); err != nil { | 			if err := s.fileCache.Remove(ids...); err != nil { | ||||||
| 				log.Warn("Error deleting attachments: %s", err.Error()) | 				log.Warn("Error deleting attachments: %s", err.Error()) | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			log.Warn("Error retrieving expired attachments: %s", err.Error()) | 			log.Debug("No expired attachments to delete") | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Prune message cache | 	// Prune message cache | ||||||
| 	olderThan := time.Now().Add(-1 * s.config.CacheDuration) | 	olderThan := time.Now().Add(-1 * s.config.CacheDuration) | ||||||
|  | 	log.Debug("Pruning messages older tha %v", olderThan) | ||||||
| 	if err := s.messageCache.Prune(olderThan); err != nil { | 	if err := s.messageCache.Prune(olderThan); err != nil { | ||||||
| 		log.Warn("Error pruning cache: %s", err.Error()) | 		log.Warn("Error pruning cache: %s", err.Error()) | ||||||
| 	} | 	} | ||||||
|  | @ -1079,6 +1099,7 @@ func (s *Server) runManager() { | ||||||
| 	for { | 	for { | ||||||
| 		select { | 		select { | ||||||
| 		case <-time.After(s.config.ManagerInterval): | 		case <-time.After(s.config.ManagerInterval): | ||||||
|  | 			log.Debug("Running manager") | ||||||
| 			s.updateStatsAndPrune() | 			s.updateStatsAndPrune() | ||||||
| 		case <-s.closeChan: | 		case <-s.closeChan: | ||||||
| 			return | 			return | ||||||
|  | @ -1124,7 +1145,7 @@ func (s *Server) sendDelayedMessages() error { | ||||||
| 	for _, m := range messages { | 	for _, m := range messages { | ||||||
| 		v := s.visitorFromIP(m.Sender) | 		v := s.visitorFromIP(m.Sender) | ||||||
| 		if err := s.sendDelayedMessage(v, m); err != nil { | 		if err := s.sendDelayedMessage(v, m); err != nil { | ||||||
| 			log.Warn("error sending delayed message: %s", err.Error()) | 			log.Warn("%s Error sending delayed message: %s", logPrefix(v, m), err.Error()) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
|  | @ -1133,12 +1154,13 @@ func (s *Server) sendDelayedMessages() error { | ||||||
| func (s *Server) sendDelayedMessage(v *visitor, m *message) error { | func (s *Server) sendDelayedMessage(v *visitor, m *message) error { | ||||||
| 	s.mu.Lock() | 	s.mu.Lock() | ||||||
| 	defer s.mu.Unlock() | 	defer s.mu.Unlock() | ||||||
|  | 	log.Debug("%s Sending delayed message", logPrefix(v, m)) | ||||||
| 	t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published | 	t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published | ||||||
| 	if ok { | 	if ok { | ||||||
| 		go func() { | 		go func() { | ||||||
| 			// We do not rate-limit messages here, since we've rate limited them in the PUT/POST handler | 			// We do not rate-limit messages here, since we've rate limited them in the PUT/POST handler | ||||||
| 			if err := t.Publish(v, m); err != nil { | 			if err := t.Publish(v, m); err != nil { | ||||||
| 				log.Warn("unable to publish message %s to topic %s: %v", m.ID, m.Topic, err.Error()) | 				log.Warn("%s Unable to publish message: %v", logPrefix(v, m), err.Error()) | ||||||
| 			} | 			} | ||||||
| 		}() | 		}() | ||||||
| 	} | 	} | ||||||
|  | @ -1311,3 +1333,7 @@ func (s *Server) visitorFromIP(ip string) *visitor { | ||||||
| 	v.Keepalive() | 	v.Keepalive() | ||||||
| 	return v | 	return v | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func logPrefix(v *visitor, m *message) string { | ||||||
|  | 	return fmt.Sprintf("%s/%s/%s", v.ip, m.Topic, m.ID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -178,3 +178,8 @@ | ||||||
| # | # | ||||||
| # visitor-attachment-total-size-limit: "100M" | # visitor-attachment-total-size-limit: "100M" | ||||||
| # visitor-attachment-daily-bandwidth-limit: "500M" | # visitor-attachment-daily-bandwidth-limit: "500M" | ||||||
|  | 
 | ||||||
|  | # Log level, can be DEBUG, INFO, WARN or ERROR | ||||||
|  | # This option can be hot-reloaded by calling "kill -HUP $pid" or "systemctl reload ntfy". | ||||||
|  | # | ||||||
|  | # log-level: INFO | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| package server | package server | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"log" | 	"heckel.io/ntfy/log" | ||||||
| 	"math/rand" | 	"math/rand" | ||||||
| 	"sync" | 	"sync" | ||||||
| ) | ) | ||||||
|  | @ -46,11 +46,16 @@ func (t *topic) Publish(v *visitor, m *message) error { | ||||||
| 	go func() { | 	go func() { | ||||||
| 		t.mu.Lock() | 		t.mu.Lock() | ||||||
| 		defer t.mu.Unlock() | 		defer t.mu.Unlock() | ||||||
|  | 		if len(t.subscribers) > 0 { | ||||||
|  | 			log.Debug("%s Forwarding to %d subscriber(s)", logPrefix(v, m), len(t.subscribers)) | ||||||
| 			for _, s := range t.subscribers { | 			for _, s := range t.subscribers { | ||||||
| 				if err := s(v, m); err != nil { | 				if err := s(v, m); err != nil { | ||||||
| 				log.Printf("error publishing message to subscriber") | 					log.Warn("%s Error forwarding to subscriber", logPrefix(v, m)) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | 		} else { | ||||||
|  | 			log.Debug("%s No subscribers, not forwarding", logPrefix(v, m)) | ||||||
|  | 		} | ||||||
| 	}() | 	}() | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue