Browse Source

Get rid of os.Exit

DarthSim 5 years ago
parent
commit
8e3cf54d85
14 changed files with 225 additions and 109 deletions
  1. 3 0
      CHANGELOG.md
  2. 73 57
      config.go
  3. 13 3
      download.go
  4. 4 3
      gcs_transport.go
  5. 10 5
      gzippool.go
  6. 4 4
      healthcheck.go
  7. 9 2
      log.go
  8. 64 13
      main.go
  9. 6 3
      newrelic.go
  10. 7 2
      processing_handler.go
  11. 13 6
      prometheus.go
  12. 4 3
      s3transport.go
  13. 8 5
      server.go
  14. 7 3
      vips.go

+ 3 - 0
CHANGELOG.md

@@ -1,6 +1,9 @@
 # Changelog
 
 ## [Unreleased]
+### Changed
+- `imgproxy -v` is replaced with `imgproxy version`.
+
 ### Fixed
 - Fix loadind BMP stored in ICO.
 - Fix ambiguous HEIC magic bytes (MP4 videos has been detected as HEIC).

+ 73 - 57
config.go

@@ -58,7 +58,7 @@ func boolEnvConfig(b *bool, name string) {
 	}
 }
 
-func hexEnvConfig(b *[]securityKey, name string) {
+func hexEnvConfig(b *[]securityKey, name string) error {
 	var err error
 
 	if env := os.Getenv(name); len(env) > 0 {
@@ -68,22 +68,24 @@ func hexEnvConfig(b *[]securityKey, name string) {
 
 		for i, part := range parts {
 			if keys[i], err = hex.DecodeString(part); err != nil {
-				logFatal("%s expected to be hex-encoded strings. Invalid: %s\n", name, part)
+				return fmt.Errorf("%s expected to be hex-encoded strings. Invalid: %s\n", name, part)
 			}
 		}
 
 		*b = keys
 	}
+
+	return nil
 }
 
-func hexFileConfig(b *[]securityKey, filepath string) {
+func hexFileConfig(b *[]securityKey, filepath string) error {
 	if len(filepath) == 0 {
-		return
+		return nil
 	}
 
 	f, err := os.Open(filepath)
 	if err != nil {
-		logFatal("Can't open file %s\n", filepath)
+		return fmt.Errorf("Can't open file %s\n", filepath)
 	}
 
 	keys := []securityKey{}
@@ -99,49 +101,55 @@ func hexFileConfig(b *[]securityKey, filepath string) {
 		if key, err := hex.DecodeString(part); err == nil {
 			keys = append(keys, key)
 		} else {
-			logFatal("%s expected to contain hex-encoded strings. Invalid: %s\n", filepath, part)
+			return fmt.Errorf("%s expected to contain hex-encoded strings. Invalid: %s\n", filepath, part)
 		}
 	}
 
 	if err := scanner.Err(); err != nil {
-		logFatal("Failed to read file %s: %s", filepath, err)
+		return fmt.Errorf("Failed to read file %s: %s", filepath, err)
 	}
 
 	*b = keys
+
+	return nil
 }
 
-func presetEnvConfig(p presets, name string) {
+func presetEnvConfig(p presets, name string) error {
 	if env := os.Getenv(name); len(env) > 0 {
 		presetStrings := strings.Split(env, ",")
 
 		for _, presetStr := range presetStrings {
 			if err := parsePreset(p, presetStr); err != nil {
-				logFatal(err.Error())
+				return fmt.Errorf(err.Error())
 			}
 		}
 	}
+
+	return nil
 }
 
-func presetFileConfig(p presets, filepath string) {
+func presetFileConfig(p presets, filepath string) error {
 	if len(filepath) == 0 {
-		return
+		return nil
 	}
 
 	f, err := os.Open(filepath)
 	if err != nil {
-		logFatal("Can't open file %s\n", filepath)
+		return fmt.Errorf("Can't open file %s\n", filepath)
 	}
 
 	scanner := bufio.NewScanner(f)
 	for scanner.Scan() {
 		if err := parsePreset(p, scanner.Text()); err != nil {
-			logFatal(err.Error())
+			return fmt.Errorf(err.Error())
 		}
 	}
 
 	if err := scanner.Err(); err != nil {
-		logFatal("Failed to read presets file: %s", err)
+		return fmt.Errorf("Failed to read presets file: %s", err)
 	}
+
+	return nil
 }
 
 type config struct {
@@ -263,18 +271,12 @@ var conf = config{
 	BufferPoolCalibrationThreshold: 1024,
 }
 
-func configure() {
+func configure() error {
 	keyPath := flag.String("keypath", "", "path of the file with hex-encoded key")
 	saltPath := flag.String("saltpath", "", "path of the file with hex-encoded salt")
 	presetsPath := flag.String("presets", "", "path of the file with presets")
-	showVersion := flag.Bool("v", false, "show version")
 	flag.Parse()
 
-	if *showVersion {
-		fmt.Println(version)
-		os.Exit(0)
-	}
-
 	if port := os.Getenv("PORT"); len(port) > 0 {
 		conf.Bind = fmt.Sprintf(":%s", port)
 	}
@@ -321,12 +323,20 @@ func configure() {
 	boolEnvConfig(&conf.UseLinearColorspace, "IMGPROXY_USE_LINEAR_COLORSPACE")
 	boolEnvConfig(&conf.DisableShrinkOnLoad, "IMGPROXY_DISABLE_SHRINK_ON_LOAD")
 
-	hexEnvConfig(&conf.Keys, "IMGPROXY_KEY")
-	hexEnvConfig(&conf.Salts, "IMGPROXY_SALT")
+	if err := hexEnvConfig(&conf.Keys, "IMGPROXY_KEY"); err != nil {
+		return err
+	}
+	if err := hexEnvConfig(&conf.Salts, "IMGPROXY_SALT"); err != nil {
+		return err
+	}
 	intEnvConfig(&conf.SignatureSize, "IMGPROXY_SIGNATURE_SIZE")
 
-	hexFileConfig(&conf.Keys, *keyPath)
-	hexFileConfig(&conf.Salts, *saltPath)
+	if err := hexFileConfig(&conf.Keys, *keyPath); err != nil {
+		return err
+	}
+	if err := hexFileConfig(&conf.Salts, *saltPath); err != nil {
+		return err
+	}
 
 	strEnvConfig(&conf.Secret, "IMGPROXY_SECRET")
 
@@ -350,8 +360,12 @@ func configure() {
 
 	strEnvConfig(&conf.BaseURL, "IMGPROXY_BASE_URL")
 
-	presetEnvConfig(conf.Presets, "IMGPROXY_PRESETS")
-	presetFileConfig(conf.Presets, *presetsPath)
+	if err := presetEnvConfig(conf.Presets, "IMGPROXY_PRESETS"); err != nil {
+		return err
+	}
+	if err := presetFileConfig(conf.Presets, *presetsPath); err != nil {
+		return err
+	}
 	boolEnvConfig(&conf.OnlyPresets, "IMGPROXY_ONLY_PRESETS")
 
 	strEnvConfig(&conf.WatermarkData, "IMGPROXY_WATERMARK_DATA")
@@ -379,7 +393,7 @@ func configure() {
 	intEnvConfig(&conf.BufferPoolCalibrationThreshold, "IMGPROXY_BUFFER_POOL_CALIBRATION_THRESHOLD")
 
 	if len(conf.Keys) != len(conf.Salts) {
-		logFatal("Number of keys and number of salts should be equal. Keys: %d, salts: %d", len(conf.Keys), len(conf.Salts))
+		return fmt.Errorf("Number of keys and number of salts should be equal. Keys: %d, salts: %d", len(conf.Keys), len(conf.Salts))
 	}
 	if len(conf.Keys) == 0 {
 		logWarning("No keys defined, so signature checking is disabled")
@@ -391,30 +405,30 @@ func configure() {
 	}
 
 	if conf.SignatureSize < 1 || conf.SignatureSize > 32 {
-		logFatal("Signature size should be within 1 and 32, now - %d\n", conf.SignatureSize)
+		return fmt.Errorf("Signature size should be within 1 and 32, now - %d\n", conf.SignatureSize)
 	}
 
 	if len(conf.Bind) == 0 {
-		logFatal("Bind address is not defined")
+		return fmt.Errorf("Bind address is not defined")
 	}
 
 	if conf.ReadTimeout <= 0 {
-		logFatal("Read timeout should be greater than 0, now - %d\n", conf.ReadTimeout)
+		return fmt.Errorf("Read timeout should be greater than 0, now - %d\n", conf.ReadTimeout)
 	}
 
 	if conf.WriteTimeout <= 0 {
-		logFatal("Write timeout should be greater than 0, now - %d\n", conf.WriteTimeout)
+		return fmt.Errorf("Write timeout should be greater than 0, now - %d\n", conf.WriteTimeout)
 	}
 	if conf.KeepAliveTimeout < 0 {
-		logFatal("KeepAlive timeout should be greater than or equal to 0, now - %d\n", conf.KeepAliveTimeout)
+		return fmt.Errorf("KeepAlive timeout should be greater than or equal to 0, now - %d\n", conf.KeepAliveTimeout)
 	}
 
 	if conf.DownloadTimeout <= 0 {
-		logFatal("Download timeout should be greater than 0, now - %d\n", conf.DownloadTimeout)
+		return fmt.Errorf("Download timeout should be greater than 0, now - %d\n", conf.DownloadTimeout)
 	}
 
 	if conf.Concurrency <= 0 {
-		logFatal("Concurrency should be greater than 0, now - %d\n", conf.Concurrency)
+		return fmt.Errorf("Concurrency should be greater than 0, now - %d\n", conf.Concurrency)
 	}
 
 	if conf.MaxClients <= 0 {
@@ -422,43 +436,43 @@ func configure() {
 	}
 
 	if conf.TTL <= 0 {
-		logFatal("TTL should be greater than 0, now - %d\n", conf.TTL)
+		return fmt.Errorf("TTL should be greater than 0, now - %d\n", conf.TTL)
 	}
 
 	if conf.MaxSrcDimension < 0 {
-		logFatal("Max src dimension should be greater than or equal to 0, now - %d\n", conf.MaxSrcDimension)
+		return fmt.Errorf("Max src dimension should be greater than or equal to 0, now - %d\n", conf.MaxSrcDimension)
 	} else if conf.MaxSrcDimension > 0 {
 		logWarning("IMGPROXY_MAX_SRC_DIMENSION is deprecated and can be removed in future versions. Use IMGPROXY_MAX_SRC_RESOLUTION")
 	}
 
 	if conf.MaxSrcResolution <= 0 {
-		logFatal("Max src resolution should be greater than 0, now - %d\n", conf.MaxSrcResolution)
+		return fmt.Errorf("Max src resolution should be greater than 0, now - %d\n", conf.MaxSrcResolution)
 	}
 
 	if conf.MaxSrcFileSize < 0 {
-		logFatal("Max src file size should be greater than or equal to 0, now - %d\n", conf.MaxSrcFileSize)
+		return fmt.Errorf("Max src file size should be greater than or equal to 0, now - %d\n", conf.MaxSrcFileSize)
 	}
 
 	if conf.MaxAnimationFrames <= 0 {
-		logFatal("Max animation frames should be greater than 0, now - %d\n", conf.MaxAnimationFrames)
+		return fmt.Errorf("Max animation frames should be greater than 0, now - %d\n", conf.MaxAnimationFrames)
 	}
 
 	if conf.PngQuantizationColors < 2 {
-		logFatal("Png quantization colors should be greater than 1, now - %d\n", conf.PngQuantizationColors)
+		return fmt.Errorf("Png quantization colors should be greater than 1, now - %d\n", conf.PngQuantizationColors)
 	} else if conf.PngQuantizationColors > 256 {
-		logFatal("Png quantization colors can't be greater than 256, now - %d\n", conf.PngQuantizationColors)
+		return fmt.Errorf("Png quantization colors can't be greater than 256, now - %d\n", conf.PngQuantizationColors)
 	}
 
 	if conf.Quality <= 0 {
-		logFatal("Quality should be greater than 0, now - %d\n", conf.Quality)
+		return fmt.Errorf("Quality should be greater than 0, now - %d\n", conf.Quality)
 	} else if conf.Quality > 100 {
-		logFatal("Quality can't be greater than 100, now - %d\n", conf.Quality)
+		return fmt.Errorf("Quality can't be greater than 100, now - %d\n", conf.Quality)
 	}
 
 	if conf.GZipCompression < 0 {
-		logFatal("GZip compression should be greater than or equal to 0, now - %d\n", conf.GZipCompression)
+		return fmt.Errorf("GZip compression should be greater than or equal to 0, now - %d\n", conf.GZipCompression)
 	} else if conf.GZipCompression > 9 {
-		logFatal("GZip compression can't be greater than 9, now - %d\n", conf.GZipCompression)
+		return fmt.Errorf("GZip compression can't be greater than 9, now - %d\n", conf.GZipCompression)
 	}
 
 	if conf.GZipCompression > 0 {
@@ -473,11 +487,11 @@ func configure() {
 		stat, err := os.Stat(conf.LocalFileSystemRoot)
 
 		if err != nil {
-			logFatal("Cannot use local directory: %s", err)
+			return fmt.Errorf("Cannot use local directory: %s", err)
 		}
 
 		if !stat.IsDir() {
-			logFatal("Cannot use local directory: not a directory")
+			return fmt.Errorf("Cannot use local directory: not a directory")
 		}
 
 		if conf.LocalFileSystemRoot == "/" {
@@ -491,32 +505,34 @@ func configure() {
 	}
 
 	if conf.WatermarkOpacity <= 0 {
-		logFatal("Watermark opacity should be greater than 0")
+		return fmt.Errorf("Watermark opacity should be greater than 0")
 	} else if conf.WatermarkOpacity > 1 {
-		logFatal("Watermark opacity should be less than or equal to 1")
+		return fmt.Errorf("Watermark opacity should be less than or equal to 1")
 	}
 
 	if len(conf.PrometheusBind) > 0 && conf.PrometheusBind == conf.Bind {
-		logFatal("Can't use the same binding for the main server and Prometheus")
+		return fmt.Errorf("Can't use the same binding for the main server and Prometheus")
 	}
 
 	if conf.FreeMemoryInterval <= 0 {
-		logFatal("Free memory interval should be greater than zero")
+		return fmt.Errorf("Free memory interval should be greater than zero")
 	}
 
 	if conf.DownloadBufferSize < 0 {
-		logFatal("Download buffer size should be greater than or equal to 0")
+		return fmt.Errorf("Download buffer size should be greater than or equal to 0")
 	} else if conf.DownloadBufferSize > math.MaxInt32 {
-		logFatal("Download buffer size can't be greater than %d", math.MaxInt32)
+		return fmt.Errorf("Download buffer size can't be greater than %d", math.MaxInt32)
 	}
 
 	if conf.GZipBufferSize < 0 {
-		logFatal("GZip buffer size should be greater than or equal to 0")
+		return fmt.Errorf("GZip buffer size should be greater than or equal to 0")
 	} else if conf.GZipBufferSize > math.MaxInt32 {
-		logFatal("GZip buffer size can't be greater than %d", math.MaxInt32)
+		return fmt.Errorf("GZip buffer size can't be greater than %d", math.MaxInt32)
 	}
 
 	if conf.BufferPoolCalibrationThreshold < 64 {
-		logFatal("Buffer pool calibration threshold should be greater than or equal to 64")
+		return fmt.Errorf("Buffer pool calibration threshold should be greater than or equal to 64")
 	}
+
+	return nil
 }

+ 13 - 3
download.go

@@ -59,7 +59,7 @@ func (lr *limitReader) Read(p []byte) (n int, err error) {
 	return
 }
 
-func initDownloading() {
+func initDownloading() error {
 	transport := &http.Transport{
 		Proxy:               http.ProxyFromEnvironment,
 		MaxIdleConns:        conf.Concurrency,
@@ -77,11 +77,19 @@ func initDownloading() {
 	}
 
 	if conf.S3Enabled {
-		transport.RegisterProtocol("s3", newS3Transport())
+		if t, err := newS3Transport(); err != nil {
+			return err
+		} else {
+			transport.RegisterProtocol("s3", t)
+		}
 	}
 
 	if conf.GCSEnabled {
-		transport.RegisterProtocol("gs", newGCSTransport())
+		if t, err := newGCSTransport(); err != nil {
+			return err
+		} else {
+			transport.RegisterProtocol("gs", t)
+		}
 	}
 
 	downloadClient = &http.Client{
@@ -92,6 +100,8 @@ func initDownloading() {
 	downloadBufPool = newBufPool("download", conf.Concurrency, conf.DownloadBufferSize)
 
 	imagemeta.SetMaxSvgCheckRead(conf.MaxSvgCheckBytes)
+
+	return nil
 }
 
 func checkDimensions(width, height int) error {

+ 4 - 3
gcs_transport.go

@@ -2,6 +2,7 @@ package main
 
 import (
 	"context"
+	"fmt"
 	"net/http"
 	"strconv"
 	"strings"
@@ -14,7 +15,7 @@ type gcsTransport struct {
 	client *storage.Client
 }
 
-func newGCSTransport() http.RoundTripper {
+func newGCSTransport() (http.RoundTripper, error) {
 	var (
 		client *storage.Client
 		err    error
@@ -27,10 +28,10 @@ func newGCSTransport() http.RoundTripper {
 	}
 
 	if err != nil {
-		logFatal("Can't create GCS client: %s", err)
+		return nil, fmt.Errorf("Can't create GCS client: %s", err)
 	}
 
-	return gcsTransport{client}
+	return gcsTransport{client}, nil
 }
 
 func (t gcsTransport) RoundTrip(req *http.Request) (*http.Response, error) {

+ 10 - 5
gzippool.go

@@ -2,6 +2,7 @@ package main
 
 import (
 	"compress/gzip"
+	"fmt"
 	"io"
 	"io/ioutil"
 	"sync"
@@ -17,26 +18,30 @@ type gzipPoolEntry struct {
 	next *gzipPoolEntry
 }
 
-func newGzipPool(n int) *gzipPool {
+func newGzipPool(n int) (*gzipPool, error) {
 	pool := new(gzipPool)
 
 	for i := 0; i < n; i++ {
-		pool.grow()
+		if err := pool.grow(); err != nil {
+			return nil, err
+		}
 	}
 
-	return pool
+	return pool, nil
 }
 
-func (p *gzipPool) grow() {
+func (p *gzipPool) grow() error {
 	gz, err := gzip.NewWriterLevel(ioutil.Discard, conf.GZipCompression)
 	if err != nil {
-		logFatal("Can't init GZip compression: %s", err)
+		return fmt.Errorf("Can't init GZip compression: %s", err)
 	}
 
 	p.top = &gzipPoolEntry{
 		gz:   gz,
 		next: p.top,
 	}
+
+	return nil
 }
 
 func (p *gzipPool) Get(w io.Writer) *gzip.Writer {

+ 4 - 4
healthcheck.go

@@ -9,7 +9,7 @@ import (
 	"os"
 )
 
-func healthcheck() {
+func healthcheck() int {
 	network := conf.Network
 	bind := conf.Bind
 
@@ -27,7 +27,7 @@ func healthcheck() {
 	res, err := httpc.Get("http://imgproxy/health")
 	if err != nil {
 		fmt.Fprintln(os.Stderr, err.Error())
-		os.Exit(1)
+		return 1
 	}
 	defer res.Body.Close()
 
@@ -35,8 +35,8 @@ func healthcheck() {
 	fmt.Fprintln(os.Stderr, string(msg))
 
 	if res.StatusCode != 200 {
-		os.Exit(1)
+		return 1
 	}
 
-	os.Exit(0)
+	return 0
 }

+ 9 - 2
log.go

@@ -1,12 +1,13 @@
 package main
 
 import (
+	"fmt"
 	"net/http"
 
 	logrus "github.com/sirupsen/logrus"
 )
 
-func initLog() {
+func initLog() error {
 	logFormat := "pretty"
 	strEnvConfig(&logFormat, "IMGPROXY_LOG_FORMAT")
 
@@ -32,11 +33,13 @@ func initLog() {
 	if isSyslogEnabled() {
 		slHook, err := newSyslogHook()
 		if err != nil {
-			logFatal("Unable to connect to local syslog daemon")
+			return fmt.Errorf("Unable to connect to syslog daemon: %s", err)
 		}
 
 		logrus.AddHook(slHook)
 	}
+
+	return nil
 }
 
 func logRequest(reqID string, r *http.Request) {
@@ -96,6 +99,10 @@ func logWarning(f string, args ...interface{}) {
 	logrus.Warnf(f, args...)
 }
 
+func logError(f string, args ...interface{}) {
+	logrus.Errorf(f, args...)
+}
+
 func logFatal(f string, args ...interface{}) {
 	logrus.Fatalf(f, args...)
 }

+ 64 - 13
main.go

@@ -1,6 +1,8 @@
 package main
 
 import (
+	"context"
+	"fmt"
 	"log"
 	"os"
 	"os/signal"
@@ -13,29 +15,46 @@ const version = "2.10.0"
 
 type ctxKey string
 
-func initialize() {
+func initialize() error {
 	log.SetOutput(os.Stdout)
 
-	initLog()
-	configure()
-	initNewrelic()
+	if err := initLog(); err != nil {
+		return err
+	}
+
+	if err := configure(); err != nil {
+		return err
+	}
+
+	if err := initNewrelic(); err != nil {
+		return err
+	}
+
 	initPrometheus()
-	initDownloading()
+
+	if err := initDownloading(); err != nil {
+		return err
+	}
+
 	initErrorsReporting()
-	initVips()
+
+	if err := initVips(); err != nil {
+		return err
+	}
 
 	if err := checkPresets(conf.Presets); err != nil {
 		shutdownVips()
-		logFatal(err.Error())
+		return err
 	}
+
+	return nil
 }
 
-func main() {
-	if len(os.Args) > 1 && os.Args[1] == "health" {
-		healthcheck()
+func run() error {
+	if err := initialize(); err != nil {
+		return err
 	}
 
-	initialize()
 	defer shutdownVips()
 
 	go func() {
@@ -52,11 +71,43 @@ func main() {
 		}
 	}()
 
-	s := startServer()
+	ctx, cancel := context.WithCancel(context.Background())
+
+	if prometheusEnabled {
+		if err := startPrometheusServer(cancel); err != nil {
+			return err
+		}
+	}
+
+	s, err := startServer(cancel)
+	if err != nil {
+		return err
+	}
 	defer shutdownServer(s)
 
 	stop := make(chan os.Signal, 1)
 	signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
 
-	<-stop
+	select {
+	case <-ctx.Done():
+	case <-stop:
+	}
+
+	return nil
+}
+
+func main() {
+	if len(os.Args) > 1 {
+		switch os.Args[1] {
+		case "health":
+			os.Exit(healthcheck())
+		case "version":
+			fmt.Println(version)
+			os.Exit(0)
+		}
+	}
+
+	if err := run(); err != nil {
+		logFatal(err.Error())
+	}
 }

+ 6 - 3
newrelic.go

@@ -2,6 +2,7 @@ package main
 
 import (
 	"context"
+	"fmt"
 	"net/http"
 	"time"
 
@@ -16,9 +17,9 @@ var (
 	newRelicTransactionCtxKey = ctxKey("newRelicTransaction")
 )
 
-func initNewrelic() {
+func initNewrelic() error {
 	if len(conf.NewRelicKey) == 0 {
-		return
+		return nil
 	}
 
 	name := conf.NewRelicAppName
@@ -32,10 +33,12 @@ func initNewrelic() {
 	newRelicApp, err = newrelic.NewApplication(config)
 
 	if err != nil {
-		logFatal("Can't init New Relic agent: %s", err)
+		return fmt.Errorf("Can't init New Relic agent: %s", err)
 	}
 
 	newRelicEnabled = true
+
+	return nil
 }
 
 func startNewRelicTransaction(ctx context.Context, rw http.ResponseWriter, r *http.Request) (context.Context, context.CancelFunc) {

+ 7 - 2
processing_handler.go

@@ -18,12 +18,15 @@ var (
 	headerVaryValue string
 )
 
-func initProcessingHandler() {
+func initProcessingHandler() error {
 	processingSem = make(chan struct{}, conf.Concurrency)
 
 	if conf.GZipCompression > 0 {
+		var err error
 		responseGzipBufPool = newBufPool("gzip", conf.Concurrency, conf.GZipBufferSize)
-		responseGzipPool = newGzipPool(conf.Concurrency)
+		if responseGzipPool, err = newGzipPool(conf.Concurrency); err != nil {
+			return err
+		}
 	}
 
 	vary := make([]string, 0)
@@ -41,6 +44,8 @@ func initProcessingHandler() {
 	}
 
 	headerVaryValue = strings.Join(vary, ", ")
+
+	return nil
 }
 
 func respondWithImage(ctx context.Context, reqID string, r *http.Request, rw http.ResponseWriter, data []byte) {

+ 13 - 6
prometheus.go

@@ -1,6 +1,8 @@
 package main
 
 import (
+	"context"
+	"fmt"
 	"net/http"
 	"time"
 
@@ -99,20 +101,25 @@ func initPrometheus() {
 	)
 
 	prometheusEnabled = true
+}
 
+func startPrometheusServer(cancel context.CancelFunc) error {
 	s := http.Server{Handler: promhttp.Handler()}
 
-	go func() {
-		l, err := listenReuseport("tcp", conf.PrometheusBind)
-		if err != nil {
-			logFatal(err.Error())
-		}
+	l, err := listenReuseport("tcp", conf.PrometheusBind)
+	if err != nil {
+		return fmt.Errorf("Can't start Prometheus metrics server: %s", err)
+	}
 
+	go func() {
 		logNotice("Starting Prometheus server at %s", conf.PrometheusBind)
 		if err := s.Serve(l); err != nil && err != http.ErrServerClosed {
-			logFatal(err.Error())
+			logError(err.Error())
 		}
+		cancel()
 	}()
+
+	return nil
 }
 
 func startPrometheusDuration(m prometheus.Histogram) func() {

+ 4 - 3
s3transport.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"fmt"
 	http "net/http"
 
 	"github.com/aws/aws-sdk-go/aws"
@@ -13,7 +14,7 @@ type s3Transport struct {
 	svc *s3.S3
 }
 
-func newS3Transport() http.RoundTripper {
+func newS3Transport() (http.RoundTripper, error) {
 	s3Conf := aws.NewConfig()
 
 	if len(conf.S3Region) != 0 {
@@ -27,14 +28,14 @@ func newS3Transport() http.RoundTripper {
 
 	sess, err := session.NewSession()
 	if err != nil {
-		logFatal("Can't create S3 session: %s", err)
+		return nil, fmt.Errorf("Can't create S3 session: %s", err)
 	}
 
 	if sess.Config.Region == nil || len(*sess.Config.Region) == 0 {
 		sess.Config.Region = aws.String("us-west-1")
 	}
 
-	return s3Transport{s3.New(sess, s3Conf)}
+	return s3Transport{s3.New(sess, s3Conf)}, nil
 }
 
 func (t s3Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {

+ 8 - 5
server.go

@@ -31,10 +31,10 @@ func buildRouter() *router {
 	return r
 }
 
-func startServer() *http.Server {
+func startServer(cancel context.CancelFunc) (*http.Server, error) {
 	l, err := listenReuseport(conf.Network, conf.Bind)
 	if err != nil {
-		logFatal(err.Error())
+		return nil, fmt.Errorf("Can't start server: %s", err)
 	}
 	l = netutil.LimitListener(l, conf.MaxClients)
 
@@ -50,16 +50,19 @@ func startServer() *http.Server {
 		s.SetKeepAlivesEnabled(false)
 	}
 
-	initProcessingHandler()
+	if err := initProcessingHandler(); err != nil {
+		return nil, err
+	}
 
 	go func() {
 		logNotice("Starting server at %s", conf.Bind)
 		if err := s.Serve(l); err != nil && err != http.ErrServerClosed {
-			logFatal(err.Error())
+			logError(err.Error())
 		}
+		cancel()
 	}()
 
-	return s
+	return s, nil
 }
 
 func shutdownServer(s *http.Server) {

+ 7 - 3
vips.go

@@ -9,6 +9,7 @@ package main
 import "C"
 import (
 	"context"
+	"fmt"
 	"math"
 	"os"
 	"runtime"
@@ -43,13 +44,13 @@ const (
 	vipsAngleD270 = C.VIPS_ANGLE_D270
 )
 
-func initVips() {
+func initVips() error {
 	runtime.LockOSThread()
 	defer runtime.UnlockOSThread()
 
 	if err := C.vips_initialize(); err != 0 {
 		C.vips_shutdown()
-		logFatal("unable to start vips!")
+		return fmt.Errorf("unable to start vips!")
 	}
 
 	// Disable libvips cache. Since processing pipeline is fine tuned, we won't get much profit from it.
@@ -95,10 +96,13 @@ func initVips() {
 	vipsConf.WatermarkOpacity = C.double(conf.WatermarkOpacity)
 
 	if err := vipsLoadWatermark(); err != nil {
-		logFatal(err.Error())
+		C.vips_shutdown()
+		return fmt.Errorf("Can't load watermark: %s", err)
 	}
 
 	vipsCollectMetrics()
+
+	return nil
 }
 
 func shutdownVips() {