Browse Source

- moved backed config from CmdConfig to AppConfig, including backend reloading event handler, updates tests
- start progress on new API, docs here https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package

flashmob 8 years ago
parent
commit
df0832764d
10 changed files with 446 additions and 78 deletions
  1. 2 2
      README.md
  2. 171 0
      api.go
  3. 168 0
      api_test.go
  4. 1 1
      backends/backend.go
  5. 1 1
      backends/gateway_test.go
  6. 3 40
      cmd/guerrillad/serve.go
  7. 11 11
      cmd/guerrillad/serve_test.go
  8. 56 20
      config.go
  9. 1 1
      config_test.go
  10. 32 2
      guerrilla.go

+ 2 - 2
README.md

@@ -190,7 +190,7 @@ func (cb *CustomBackend) Process(e *mail.Envelope) backends.Result {
 ```go
 import "github.com/flashmob/go-guerrilla/log"
 
-mainlog, err := log.GetLogger(string(log.OutputStderr));
+mainlog, err := log.GetLogger(log.OutputStderr.String());
 if  err != nil {
     fmt.Println("Cannot open log:", err)
     os.Exit(1)
@@ -201,7 +201,7 @@ if  err != nil {
 See Configuration section below for setting configuration options.
 ```go
 config := &guerrilla.AppConfig{
-  Servers: []*guerrilla.ServerConfig{...},
+  Servers: []guerrilla.ServerConfig{...},
   AllowedHosts: []string{...}
 }
 backend := &CustomBackend{...}

+ 171 - 0
api.go

@@ -0,0 +1,171 @@
+package guerrilla
+
+import (
+	"errors"
+	"fmt"
+	_ "fmt"
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
+	"io/ioutil"
+	"os"
+)
+
+type SMTP struct {
+	config  *AppConfig
+	logger  log.Logger
+	backend backends.Backend
+	g       Guerrilla
+}
+
+const defaultInterface = "127.0.0.1:2525"
+
+// configureDefaults fills in default server settings for values that were not configured
+func (s *SMTP) configureDefaults() error {
+	if s.config.LogFile == "" {
+		s.config.LogFile = log.OutputStderr.String()
+	}
+	if s.config.LogLevel == "" {
+		s.config.LogLevel = "debug"
+	}
+	if len(s.config.AllowedHosts) == 0 {
+		if h, err := os.Hostname(); err != nil {
+			return err
+		} else {
+			s.config.AllowedHosts = append(s.config.AllowedHosts, h)
+		}
+	}
+	h, err := os.Hostname()
+	if err != nil {
+		return err
+	}
+	if len(s.config.Servers) == 0 {
+		sc := ServerConfig{}
+		sc.LogFile = s.config.LogFile
+		sc.ListenInterface = defaultInterface
+		sc.IsEnabled = true
+		sc.Hostname = h
+		sc.MaxClients = 100
+		sc.Timeout = 30
+		sc.MaxSize = 10 << 20 // 10 Mebibytes
+		s.config.Servers = append(s.config.Servers, sc)
+	} else {
+		// make sure each server has defaults correctly configured
+		for i := range s.config.Servers {
+			if s.config.Servers[i].Hostname == "" {
+				s.config.Servers[i].Hostname = h
+			}
+			if s.config.Servers[i].MaxClients == 0 {
+				s.config.Servers[i].MaxClients = 100
+			}
+			if s.config.Servers[i].Timeout == 0 {
+				s.config.Servers[i].Timeout = 20
+			}
+			if s.config.Servers[i].MaxSize == 0 {
+				s.config.Servers[i].MaxSize = 10 << 20 // 10 Mebibytes
+			}
+			if s.config.Servers[i].ListenInterface == "" {
+				return errors.New(fmt.Sprintf("Listen interface not specified for server at index %d", i))
+			}
+			if s.config.Servers[i].LogFile == "" {
+				s.config.Servers[i].LogFile = s.config.LogFile
+			}
+			// validate the server config
+			err = s.config.Servers[i].Validate()
+			if err != nil {
+				return err
+			}
+		}
+
+	}
+	return nil
+
+}
+
+func (s *SMTP) configureDefaultBackend() error {
+	h, err := os.Hostname()
+	if err != nil {
+		return err
+	}
+	if len(s.config.BackendConfig) == 0 {
+		bcfg := backends.BackendConfig{
+			"log_received_mails": true,
+			"save_workers_size":  1,
+			"process_stack":      "HeadersParser|Header|Debugger",
+			"primary_mail_host":  h,
+		}
+		s.backend, err = backends.New(bcfg, s.logger)
+		if err != nil {
+			return err
+		}
+	} else {
+		if _, ok := s.config.BackendConfig["process_stack"]; !ok {
+			s.config.BackendConfig["process_stack"] = "HeadersParser|Header|Debugger"
+		}
+		if _, ok := s.config.BackendConfig["primary_mail_host"]; !ok {
+			s.config.BackendConfig["primary_mail_host"] = h
+		}
+		if _, ok := s.config.BackendConfig["save_workers_size"]; !ok {
+			s.config.BackendConfig["save_workers_size"] = 1
+		}
+
+		if _, ok := s.config.BackendConfig["log_received_mails"]; !ok {
+			s.config.BackendConfig["log_received_mails"] = false
+		}
+		s.backend, err = backends.New(s.config.BackendConfig, s.logger)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (s *SMTP) Start() (err error) {
+	if s.g == nil {
+		if s.config == nil {
+			s.config = &AppConfig{}
+		}
+		err = s.configureDefaults()
+		if err != nil {
+			return err
+		}
+
+		if s.logger == nil {
+			s.logger, err = log.GetLogger(s.config.LogFile)
+			if err != nil {
+				return err
+			}
+		}
+		if s.backend == nil {
+			err = s.configureDefaultBackend()
+			if err != nil {
+				return
+			}
+		}
+		s.g, err = New(s.config, s.backend, s.logger)
+		if err != nil {
+			return err
+		}
+
+	}
+	return s.g.Start()
+}
+
+func (s *SMTP) Shutdown() {
+	s.g.Shutdown()
+}
+
+// ReadConfig reads in the config from a json file.
+func (s *SMTP) ReadConfig(path string) error {
+	data, err := ioutil.ReadFile(path)
+	if err != nil {
+		return fmt.Errorf("Could not read config file: %s", err.Error())
+	}
+	if s.config == nil {
+		s.config = &AppConfig{}
+	}
+	if err := s.config.Load(data); err != nil {
+		return err
+	}
+	return nil
+}

+ 168 - 0
api_test.go

@@ -0,0 +1,168 @@
+package guerrilla
+
+import (
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
+	"io/ioutil"
+	"testing"
+	"time"
+)
+
+// Test Starting smtp without setting up logger / backend
+func TestSMTP(t *testing.T) {
+
+	smtp := SMTP{}
+	err := smtp.Start()
+
+	if err != nil {
+		t.Error(err)
+	}
+	// it should set to stderr automatically
+	if smtp.config.LogFile != log.OutputStderr.String() {
+		t.Error("smtp.config.LogFile is not", log.OutputStderr.String())
+	}
+
+	if len(smtp.config.AllowedHosts) == 0 {
+		t.Error("smtp.config.AllowedHosts len should be 1, not 0", smtp.config.AllowedHosts)
+	}
+
+	if smtp.config.LogLevel != "debug" {
+		t.Error("smtp.config.LogLevel expected'debug', it is", smtp.config.LogLevel)
+	}
+	if len(smtp.config.Servers) != 1 {
+		t.Error("len(smtp.config.Servers) should be 1, got", len(smtp.config.Servers))
+	}
+	time.Sleep(time.Second * 2)
+	smtp.Shutdown()
+
+}
+
+// Suppressing log output
+func TestSMTPNoLog(t *testing.T) {
+
+	// configure a default server with no log output
+	cfg := &AppConfig{LogFile: log.OutputOff.String()}
+	smtp := SMTP{config: cfg}
+
+	err := smtp.Start()
+	if err != nil {
+		t.Error(err)
+	}
+	time.Sleep(time.Second * 2)
+	smtp.Shutdown()
+}
+
+// our custom server
+func TestSMTPCustomServer(t *testing.T) {
+	cfg := &AppConfig{LogFile: log.OutputStdout.String()}
+	sc := ServerConfig{
+		ListenInterface: "127.0.0.1:2526",
+		IsEnabled:       true,
+	}
+	cfg.Servers = append(cfg.Servers, sc)
+	smtp := SMTP{config: cfg}
+
+	err := smtp.Start()
+	if err != nil {
+		t.Error("start error", err)
+	} else {
+		time.Sleep(time.Second * 2)
+		smtp.Shutdown()
+	}
+
+}
+
+// with a backend config
+func TestSMTPCustomBackend(t *testing.T) {
+	cfg := &AppConfig{LogFile: log.OutputStdout.String()}
+	sc := ServerConfig{
+		ListenInterface: "127.0.0.1:2526",
+		IsEnabled:       true,
+	}
+	cfg.Servers = append(cfg.Servers, sc)
+	bcfg := backends.BackendConfig{
+		"save_workers_size":  3,
+		"process_stack":      "HeadersParser|Header|Hasher|Debugger",
+		"log_received_mails": true,
+	}
+	cfg.BackendConfig = bcfg
+	smtp := SMTP{config: cfg}
+
+	err := smtp.Start()
+	if err != nil {
+		t.Error("start error", err)
+	} else {
+		time.Sleep(time.Second * 2)
+		smtp.Shutdown()
+	}
+}
+
+// with a config from a json file
+func TestSMTPLoadFile(t *testing.T) {
+	json := `{
+    "log_file" : "./tests/testlog",
+    "log_level" : "debug",
+    "pid_file" : "/var/run/go-guerrilla.pid",
+    "allowed_hosts": ["spam4.me","grr.la"],
+    "backend_config" :
+        {
+            "log_received_mails" : true,
+            "process_stack": "HeadersParser|Header|Hasher|Debugger",
+            "save_workers_size":  3
+        },
+    "servers" : [
+        {
+            "is_enabled" : true,
+            "host_name":"mail.guerrillamail.com",
+            "max_size": 100017,
+            "private_key_file":"config_test.go",
+            "public_key_file":"config_test.go",
+            "timeout":160,
+            "listen_interface":"127.0.0.1:2526",
+            "start_tls_on":false,
+            "tls_always_on":false,
+            "max_clients": 2
+        }
+    ]
+}
+
+	`
+	err := ioutil.WriteFile("goguerrilla.conf.api", []byte(json), 0644)
+	if err != nil {
+		t.Error("could not write guerrilla.conf.api", err)
+		return
+	}
+
+	/*
+
+		cfg := &AppConfig{LogFile: log.OutputStdout.String()}
+		sc := ServerConfig{
+			ListenInterface: "127.0.0.1:2526",
+			IsEnabled:       true,
+		}
+		cfg.Servers = append(cfg.Servers, sc)
+		bcfg := backends.BackendConfig{
+			"save_workers_size":  3,
+			"process_stack":      "HeadersParser|Header|Hasher|Debugger",
+			"log_received_mails": true,
+		}
+		cfg.BackendConfig = bcfg
+
+		smtp := SMTP{config: cfg}
+	*/
+	smtp := SMTP{}
+	err = smtp.ReadConfig("goguerrilla.conf.api")
+	if err != nil {
+		t.Error("ReadConfig error", err)
+		return
+	}
+
+	err = smtp.Start()
+	if err != nil {
+		t.Error("start error", err)
+		return
+	} else {
+		time.Sleep(time.Second * 2)
+		smtp.Shutdown()
+	}
+}

+ 1 - 1
backends/backend.go

@@ -149,7 +149,7 @@ func Log() log.Logger {
 	if v, ok := Svc.mainlog.Load().(log.Logger); ok {
 		return v
 	}
-	l, _ := log.GetLogger(string(log.OutputStderr))
+	l, _ := log.GetLogger(log.OutputStderr.String())
 	return l
 }
 

+ 1 - 1
backends/gateway_test.go

@@ -58,7 +58,7 @@ func TestStartProcessStop(t *testing.T) {
 	gateway := &BackendGateway{}
 	err := gateway.Initialize(c)
 
-	mainlog, _ := log.GetLogger(string(log.OutputOff))
+	mainlog, _ := log.GetLogger(log.OutputOff.String())
 	Svc.SetMainlog(mainlog)
 
 	if err != nil {

+ 3 - 40
cmd/guerrillad/serve.go

@@ -12,7 +12,6 @@ import (
 	"os"
 	"os/exec"
 	"os/signal"
-	"reflect"
 	"strconv"
 	"strings"
 	"syscall"
@@ -94,39 +93,6 @@ func sigHandler(app guerrilla.Guerrilla) {
 	}
 }
 
-func subscribeBackendEvent(event guerrilla.Event, backend backends.Backend, app guerrilla.Guerrilla) {
-	app.Subscribe(event, func(cmdConfig *CmdConfig) {
-		logger, _ := log.GetLogger(cmdConfig.LogFile)
-		var err error
-		if err = backend.Shutdown(); err != nil {
-			logger.WithError(err).Warn("Backend failed to shutdown")
-			return
-		}
-		// init a new backend
-
-		if newBackend, newErr := backends.New(cmdConfig.BackendConfig, logger); newErr != nil {
-			// Revert to old backend config
-			logger.WithError(newErr).Error("Error while loading the backend")
-			err = backend.Reinitialize()
-			if err != nil {
-				logger.WithError(err).Fatal("failed to revert to old backend config")
-				return
-			}
-			err = backend.Start()
-			if err != nil {
-				logger.WithError(err).Fatal("failed to start backend with old config")
-				return
-			}
-			logger.Info("reverted to old backend config")
-		} else {
-			// swap to the bew backend (assuming old backend was shutdown so it can be safely swapped)
-			backend.Start()
-			backend = newBackend
-			logger.Info("new backend started")
-		}
-	})
-}
-
 func serve(cmd *cobra.Command, args []string) {
 	logVersion()
 
@@ -182,7 +148,7 @@ func serve(cmd *cobra.Command, args []string) {
 	if err != nil {
 		mainlog.WithError(err).Error("Error(s) when starting server(s)")
 	}
-	subscribeBackendEvent(guerrilla.EventConfigBackendConfig, backend, app)
+
 	// Write out our PID
 	writePid(cmdConfig.PidFile)
 	// ...and write out our pid whenever the file name changes in the config
@@ -204,7 +170,6 @@ func serve(cmd *cobra.Command, args []string) {
 // the the command line interface.
 type CmdConfig struct {
 	guerrilla.AppConfig
-	BackendConfig backends.BackendConfig `json:"backend_config"`
 }
 
 func (c *CmdConfig) load(jsonBytes []byte) error {
@@ -218,10 +183,8 @@ func (c *CmdConfig) load(jsonBytes []byte) error {
 }
 
 func (c *CmdConfig) emitChangeEvents(oldConfig *CmdConfig, app guerrilla.Guerrilla) {
-	// has backend changed?
-	if !reflect.DeepEqual((*c).BackendConfig, (*oldConfig).BackendConfig) {
-		app.Publish(guerrilla.EventConfigBackendConfig, c)
-	}
+	// if your CmdConfig has any extra fields, you can emit events here
+	// ...
 	// call other emitChangeEvents
 	c.AppConfig.EmitChangeEvents(&oldConfig.AppConfig, app)
 }

+ 11 - 11
cmd/guerrillad/serve_test.go

@@ -307,18 +307,18 @@ func sigKill() {
 // make sure that we get all the config change events
 func TestCmdConfigChangeEvents(t *testing.T) {
 
-	oldconf := &CmdConfig{}
-	if err := oldconf.load([]byte(configJsonA)); err != nil {
+	oldconf := &guerrilla.AppConfig{}
+	if err := oldconf.Load([]byte(configJsonA)); err != nil {
 		t.Error("configJsonA is invalid", err)
 	}
 
-	newconf := &CmdConfig{}
-	if err := newconf.load([]byte(configJsonB)); err != nil {
+	newconf := &guerrilla.AppConfig{}
+	if err := newconf.Load([]byte(configJsonB)); err != nil {
 		t.Error("configJsonB is invalid", err)
 	}
 
-	newerconf := &CmdConfig{}
-	if err := newerconf.load([]byte(configJsonC)); err != nil {
+	newerconf := &guerrilla.AppConfig{}
+	if err := newerconf.Load([]byte(configJsonC)); err != nil {
 		t.Error("configJsonC is invalid", err)
 	}
 
@@ -330,11 +330,11 @@ func TestCmdConfigChangeEvents(t *testing.T) {
 
 	bcfg := backends.BackendConfig{"log_received_mails": true}
 	backend, err := backends.New(bcfg, mainlog)
-	app, err := guerrilla.New(&oldconf.AppConfig, backend, mainlog)
+	app, err := guerrilla.New(oldconf, backend, mainlog)
 	if err != nil {
 		//log.Info("Failed to create new app", err)
 	}
-	toUnsubscribe := map[guerrilla.Event]func(c *CmdConfig){}
+	toUnsubscribe := map[guerrilla.Event]func(c *guerrilla.AppConfig){}
 	toUnsubscribeS := map[guerrilla.Event]func(c *guerrilla.ServerConfig){}
 
 	for event := range expectedEvents {
@@ -347,7 +347,7 @@ func TestCmdConfigChangeEvents(t *testing.T) {
 				app.Subscribe(e, f)
 				toUnsubscribeS[e] = f
 			} else {
-				f := func(c *CmdConfig) {
+				f := func(c *guerrilla.AppConfig) {
 					expectedEvents[e] = true
 				}
 				app.Subscribe(e, f)
@@ -358,8 +358,8 @@ func TestCmdConfigChangeEvents(t *testing.T) {
 	}
 
 	// emit events
-	newconf.emitChangeEvents(oldconf, app)
-	newerconf.emitChangeEvents(newconf, app)
+	newconf.EmitChangeEvents(oldconf, app)
+	newerconf.EmitChangeEvents(newconf, app)
 	// unsubscribe
 	for unevent, unfun := range toUnsubscribe {
 		app.Unsubscribe(unevent, unfun)

+ 56 - 20
config.go

@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/flashmob/go-guerrilla/backends"
 	"os"
 	"reflect"
 	"strings"
@@ -12,27 +13,48 @@ import (
 
 // AppConfig is the holder of the configuration of the app
 type AppConfig struct {
-	Servers      []ServerConfig `json:"servers"`
-	AllowedHosts []string       `json:"allowed_hosts"`
-	PidFile      string         `json:"pid_file"`
-	LogFile      string         `json:"log_file,omitempty"`
-	LogLevel     string         `json:"log_level,omitempty"`
+	// Servers can have one or more items. Defaults to 1 server listening to 127.0.0.1:2525
+	Servers []ServerConfig `json:"servers"`
+	// AllowedHosts lists which hosts to accept email for. Defaults to os.Hostname()
+	AllowedHosts []string `json:"allowed_hosts"`
+	// PidFile is the path for writing out the process id. No output if empty
+	PidFile string `json:"pid_file"`
+	// LogFile is where the logs go. Use path to file, or "stderr", "stdout" or "off". Default "stderr"
+	LogFile string `json:"log_file,omitempty"`
+	// LogLevel controls the lowest level we log. "info", "debug", "error", "panic". Default "info"
+	LogLevel string `json:"log_level,omitempty"`
+	// BackendConfig configures the transaction processing backend
+	BackendConfig backends.BackendConfig `json:"backend_config"`
 }
 
 // ServerConfig specifies config options for a single server
 type ServerConfig struct {
-	IsEnabled       bool   `json:"is_enabled"`
-	Hostname        string `json:"host_name"`
-	MaxSize         int64  `json:"max_size"`
-	PrivateKeyFile  string `json:"private_key_file"`
-	PublicKeyFile   string `json:"public_key_file"`
-	Timeout         int    `json:"timeout"`
+	// IsEnabled set to true to start the server, false will ignore it
+	IsEnabled bool `json:"is_enabled"`
+	// Hostname will be used in the server's reply to HELO/EHLO. If TLS enabled
+	// make sure that the Hostname matches the cert. Defaults to os.Hostname()
+	Hostname string `json:"host_name"`
+	// MaxSize is the maximum size of an email that will be accepted for delivery. Defaults to 10MB
+	MaxSize int64 `json:"max_size"`
+	// PrivateKeyFile path to cert private key in PEM format. Will be ignored if blank
+	PrivateKeyFile string `json:"private_key_file"`
+	// PublicKeyFile path to cert (public key) chain in PEM format. Will be ignored if blank
+	PublicKeyFile string `json:"public_key_file"`
+	// Timeout specifies the connection timeout in seconds. Defaults to 30
+	Timeout int `json:"timeout"`
+	// Listen interface specified in <ip>:<port> - defaults to 127.0.0.1:2525
 	ListenInterface string `json:"listen_interface"`
-	StartTLSOn      bool   `json:"start_tls_on,omitempty"`
-	TLSAlwaysOn     bool   `json:"tls_always_on,omitempty"`
-	MaxClients      int    `json:"max_clients"`
-	LogFile         string `json:"log_file,omitempty"`
+	// StartTLSOn should we offer STARTTLS command. Cert must be valid. False by default
+	StartTLSOn bool `json:"start_tls_on,omitempty"`
+	// TLSAlwaysOn run this server as a pure TLS server, i.e. SMTPS
+	TLSAlwaysOn bool `json:"tls_always_on,omitempty"`
+	// MaxClients controls how many maxiumum clients we can handle at once. Defaults to 100
+	MaxClients int `json:"max_clients"`
+	// LogFile is where the logs go. Use path to file, or "stderr", "stdout" or "off". Default "stderr"
+	// defaults to AppConfig.Log file setting
+	LogFile string `json:"log_file,omitempty"`
 
+	// The following used to watch certificate changes so that the TLS can be reloaded
 	_privateKeyFile_mtime int
 	_publicKeyFile_mtime  int
 }
@@ -64,6 +86,10 @@ func (c *AppConfig) Load(jsonBytes []byte) error {
 
 // Emits any configuration change events onto the event bus.
 func (c *AppConfig) EmitChangeEvents(oldConfig *AppConfig, app Guerrilla) {
+	// has backend changed?
+	if !reflect.DeepEqual((*c).BackendConfig, (*oldConfig).BackendConfig) {
+		app.Publish(EventConfigBackendConfig, c)
+	}
 	// has config changed, general check
 	if !reflect.DeepEqual(oldConfig, c) {
 		app.Publish(EventConfigNewConfig, c)
@@ -213,16 +239,26 @@ func (sc *ServerConfig) getTlsKeyTimestamps() (int, int) {
 }
 
 // Validate validates the server's configuration.
-func (sc *ServerConfig) Validate() Errors {
+func (sc *ServerConfig) Validate() error {
 	var errs Errors
-	if _, err := tls.LoadX509KeyPair(sc.PublicKeyFile, sc.PrivateKeyFile); err != nil {
-		if sc.StartTLSOn || sc.TLSAlwaysOn {
+
+	if sc.StartTLSOn || sc.TLSAlwaysOn {
+		if sc.PublicKeyFile == "" {
+			errs = append(errs, errors.New("PublicKeyFile is empty"))
+		}
+		if sc.PrivateKeyFile == "" {
+			errs = append(errs, errors.New("PrivateKeyFile is empty"))
+		}
+		if _, err := tls.LoadX509KeyPair(sc.PublicKeyFile, sc.PrivateKeyFile); err != nil {
 			errs = append(errs,
 				errors.New(fmt.Sprintf("cannot use TLS config for [%s], %v", sc.ListenInterface, err)))
 		}
-
 	}
-	return errs
+	if len(errs) > 0 {
+		return errs
+	}
+
+	return nil
 }
 
 // Returns a diff between struct a & struct b.

+ 1 - 1
config_test.go

@@ -206,7 +206,7 @@ func TestConfigChangeEvents(t *testing.T) {
 	newconf := &AppConfig{}
 	newconf.Load([]byte(configJsonB))
 	newconf.Servers[0].LogFile = "off" // test for log file change
-	newconf.LogLevel = "off"
+	newconf.LogLevel = "info"
 	newconf.LogFile = "off"
 	expectedEvents := map[Event]bool{
 		EventConfigPidFile:         false,

+ 32 - 2
guerrilla.go

@@ -106,9 +106,9 @@ func (g *guerrilla) makeServers() error {
 			// server already instantiated
 			continue
 		}
-		if errs := sc.Validate(); errs != nil {
+		if err := sc.Validate(); err != nil {
 			g.mainlog().WithError(errs).Errorf("Failed to create server [%s]", sc.ListenInterface)
-			errs = append(errs, errs...)
+			errs = append(errs, err)
 			continue
 		} else {
 			server, err := newServer(&sc, g.backend, g.mainlog())
@@ -327,6 +327,36 @@ func (g *guerrilla) subscribeEvents() {
 			g.mainlog().Infof("Server [%s] re-opened log file [%s]", sc.ListenInterface, sc.LogFile)
 		}
 	})
+	// when the backend changes
+	g.Subscribe(EventConfigBackendConfig, func(appConfig *AppConfig) {
+		logger, _ := log.GetLogger(appConfig.LogFile)
+		// shutdown the backend first.
+		var err error
+		if err = g.backend.Shutdown(); err != nil {
+			logger.WithError(err).Warn("Backend failed to shutdown")
+			return
+		}
+		// init a new backend, Revert to old backend config if it failes
+		if newBackend, newErr := backends.New(appConfig.BackendConfig, logger); newErr != nil {
+			logger.WithError(newErr).Error("Error while loading the backend")
+			err = g.backend.Reinitialize()
+			if err != nil {
+				logger.WithError(err).Fatal("failed to revert to old backend config")
+				return
+			}
+			err = g.backend.Start()
+			if err != nil {
+				logger.WithError(err).Fatal("failed to start backend with old config")
+				return
+			}
+			logger.Info("reverted to old backend config")
+		} else {
+			// swap to the bew backend (assuming old backend was shutdown so it can be safely swapped)
+			newBackend.Start()
+			g.backend = newBackend
+			logger.Info("new backend started")
+		}
+	})
 
 }