1
0
Эх сурвалжийг харах

errorreport package turned into instance

Viktor Sokolov 1 долоо хоног өмнө
parent
commit
4c2706e0d1

+ 24 - 0
env/parsers.go

@@ -300,3 +300,27 @@ func HexSlice(b *[][]byte, desc Desc) error {
 
 	return nil
 }
+
+// StringMap parses a map[string]string from the environment variable using semicolon-separated key=value pairs
+func StringMap(m *map[string]string, desc Desc) error {
+	value, ok := desc.Get()
+	if !ok {
+		return nil
+	}
+
+	mm := make(map[string]string)
+
+	keyvalues := strings.Split(value, ";")
+
+	for _, keyvalue := range keyvalues {
+		parts := strings.SplitN(keyvalue, "=", 2)
+		if len(parts) != 2 {
+			return desc.Errorf("invalid key/value: %s", keyvalue)
+		}
+		mm[parts[0]] = parts[1]
+	}
+
+	*m = mm
+
+	return nil
+}

+ 39 - 19
errorreport/airbrake/airbrake.go

@@ -1,47 +1,67 @@
 package airbrake
 
 import (
+	"errors"
 	"net/http"
 	"strings"
 
 	"github.com/airbrake/gobrake/v5"
 
-	"github.com/imgproxy/imgproxy/v3/config"
+	"github.com/imgproxy/imgproxy/v3/env"
 )
 
 var (
-	notifier *gobrake.Notifier
+	IMGPROXY_AIRBRAKE_PROJECT_ID  = env.Describe("IMGPROXY_AIRBRAKE_PROJECT_ID", "integer")
+	IMGPROXY_AIRBRAKE_PROJECT_KEY = env.Describe("IMGPROXY_AIRBRAKE_PROJECT_KEY", "string")
+	IMGPROXY_AIRBRAKE_ENV         = env.Describe("IMGPROXY_AIRBRAKE_ENV", "string")
 
 	metaReplacer = strings.NewReplacer(" ", "-")
 )
 
-func Init() {
-	if len(config.AirbrakeProjectKey) > 0 {
-		notifier = gobrake.NewNotifierWithOptions(&gobrake.NotifierOptions{
-			ProjectId:   int64(config.AirbrakeProjectID),
-			ProjectKey:  config.AirbrakeProjectKey,
-			Environment: config.AirbrakeEnv,
-		})
-	}
+type reporter struct {
+	notifier *gobrake.Notifier
 }
 
-func Report(err error, req *http.Request, meta map[string]any) {
-	if notifier == nil {
-		return
+func New() (*reporter, error) {
+	var projectID int
+	var projectKey string
+
+	projectEnv := "production"
+
+	err := errors.Join(
+		env.Int(&projectID, IMGPROXY_AIRBRAKE_PROJECT_ID),
+		env.String(&projectKey, IMGPROXY_AIRBRAKE_PROJECT_KEY),
+		env.String(&projectEnv, IMGPROXY_AIRBRAKE_ENV),
+	)
+
+	if err != nil {
+		return nil, err
 	}
 
-	notice := notifier.Notice(err, req, 2)
+	if len(projectKey) == 0 {
+		return nil, nil
+	}
+
+	notifier := gobrake.NewNotifierWithOptions(&gobrake.NotifierOptions{
+		ProjectId:   int64(projectID),
+		ProjectKey:  projectKey,
+		Environment: projectEnv,
+	})
+
+	return &reporter{notifier}, nil
+}
+
+func (r *reporter) Report(err error, req *http.Request, meta map[string]any) {
+	notice := r.notifier.Notice(err, req, 2)
 
 	for k, v := range meta {
 		key := metaReplacer.Replace(strings.ToLower(k))
 		notice.Context[key] = v
 	}
 
-	notifier.SendNoticeAsync(notice)
+	r.notifier.SendNoticeAsync(notice)
 }
 
-func Close() {
-	if notifier != nil {
-		notifier.Close()
-	}
+func (r *reporter) Close() {
+	r.notifier.Close()
 }

+ 41 - 20
errorreport/bugsnag/bugsnag.go

@@ -1,19 +1,52 @@
 package bugsnag
 
 import (
+	"errors"
 	"fmt"
 	"log/slog"
 	"net/http"
 
 	"github.com/bugsnag/bugsnag-go/v2"
 
-	"github.com/imgproxy/imgproxy/v3/config"
+	"github.com/imgproxy/imgproxy/v3/env"
 )
 
-var enabled bool
-
+// logger is the logger forwarder for bugsnag
 type logger struct{}
 
+var (
+	IMGPROXY_BUGSNAG_KEY   = env.Describe("IMGPROXY_BUGSNAG_KEY", "string")
+	IMGPROXY_BUGSNAG_STAGE = env.Describe("IMGPROXY_BUGSNAG_STAGE", "string")
+)
+
+type reporter struct{}
+
+func New() (*reporter, error) {
+	key := ""
+	stage := "production"
+
+	err := errors.Join(
+		env.String(&key, IMGPROXY_BUGSNAG_KEY),
+		env.String(&stage, IMGPROXY_BUGSNAG_STAGE),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(key) == 0 {
+		return nil, nil
+	}
+
+	bugsnag.Configure(bugsnag.Configuration{
+		APIKey:       key,
+		ReleaseStage: stage,
+		PanicHandler: func() {}, // Disable forking the process
+		Logger:       logger{},
+	})
+
+	return &reporter{}, nil
+}
+
 func (l logger) Printf(format string, v ...interface{}) {
 	slog.Debug(
 		fmt.Sprintf(format, v...),
@@ -21,23 +54,7 @@ func (l logger) Printf(format string, v ...interface{}) {
 	)
 }
 
-func Init() {
-	if len(config.BugsnagKey) > 0 {
-		bugsnag.Configure(bugsnag.Configuration{
-			APIKey:       config.BugsnagKey,
-			ReleaseStage: config.BugsnagStage,
-			PanicHandler: func() {}, // Disable forking the process
-			Logger:       logger{},
-		})
-		enabled = true
-	}
-}
-
-func Report(err error, req *http.Request, meta map[string]any) {
-	if !enabled {
-		return
-	}
-
+func (r *reporter) Report(err error, req *http.Request, meta map[string]any) {
 	extra := make(bugsnag.MetaData)
 	for k, v := range meta {
 		extra.Add("Processing Context", k, v)
@@ -45,3 +62,7 @@ func Report(err error, req *http.Request, meta map[string]any) {
 
 	bugsnag.Notify(err, req, extra)
 }
+
+func (r *reporter) Close() {
+	// noop
+}

+ 50 - 10
errorreport/errorreport.go

@@ -10,20 +10,57 @@ import (
 	"github.com/imgproxy/imgproxy/v3/errorreport/sentry"
 )
 
+// reporter is an interface that all error reporters must implement.
+// most of our reporters are singletons, so in most cases close is noop.
+type reporter interface {
+	Report(err error, req *http.Request, meta map[string]any)
+	Close()
+}
+
+// metaCtxKey is the context.Context key for request metadata
 type metaCtxKey struct{}
 
-func Init() {
-	bugsnag.Init()
-	honeybadger.Init()
-	sentry.Init()
-	airbrake.Init()
+// initialized reporters
+var reporters []reporter
+
+// New initializes all configured error reporters and returns a Reporter instance.
+func Init() error {
+	reporters = make([]reporter, 0)
+
+	if r, err := bugsnag.New(); err != nil {
+		return err
+	} else if r != nil {
+		reporters = append(reporters, r)
+	}
+
+	if r, err := honeybadger.New(); err != nil {
+		return err
+	} else if r != nil {
+		reporters = append(reporters, r)
+	}
+
+	if r, err := sentry.New(); err != nil {
+		return err
+	} else if r != nil {
+		reporters = append(reporters, r)
+	}
+
+	if r, err := airbrake.New(); err != nil {
+		return err
+	} else if r != nil {
+		reporters = append(reporters, r)
+	}
+
+	return nil
 }
 
+// StartRequest initializes metadata storage in the request context.
 func StartRequest(req *http.Request) context.Context {
 	meta := make(map[string]any)
 	return context.WithValue(req.Context(), metaCtxKey{}, meta)
 }
 
+// SetMetadata sets a metadata key-value pair in the request context.
 func SetMetadata(req *http.Request, key string, value any) {
 	meta, ok := req.Context().Value(metaCtxKey{}).(map[string]any)
 	if !ok || meta == nil {
@@ -33,18 +70,21 @@ func SetMetadata(req *http.Request, key string, value any) {
 	meta[key] = value
 }
 
+// Report reports an error to all configured reporters with the request and its metadata.
 func Report(err error, req *http.Request) {
 	meta, ok := req.Context().Value(metaCtxKey{}).(map[string]any)
 	if !ok {
 		meta = nil
 	}
 
-	bugsnag.Report(err, req, meta)
-	honeybadger.Report(err, req, meta)
-	sentry.Report(err, req, meta)
-	airbrake.Report(err, req, meta)
+	for _, reporter := range reporters {
+		reporter.Report(err, req, meta)
+	}
 }
 
+// Close closes all reporters
 func Close() {
-	airbrake.Close()
+	for _, reporter := range reporters {
+		reporter.Close()
+	}
 }

+ 31 - 13
errorreport/honeybadger/honeybadger.go

@@ -1,37 +1,51 @@
 package honeybadger
 
 import (
+	"errors"
 	"net/http"
 	"reflect"
 	"strings"
 
 	"github.com/honeybadger-io/honeybadger-go"
 
-	"github.com/imgproxy/imgproxy/v3/config"
+	"github.com/imgproxy/imgproxy/v3/env"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 )
 
 var (
-	enabled bool
+	IMGPROXY_HONEYBADGER_KEY = env.Describe("IMGPROXY_HONEYBADGER_KEY", "string")
+	IMGPROXY_HONEYBADGER_ENV = env.Describe("IMGPROXY_HONEYBADGER_ENV", "string")
 
 	metaReplacer = strings.NewReplacer("-", "_", " ", "_")
 )
 
-func Init() {
-	if len(config.HoneybadgerKey) > 0 {
-		honeybadger.Configure(honeybadger.Configuration{
-			APIKey: config.HoneybadgerKey,
-			Env:    config.HoneybadgerEnv,
-		})
-		enabled = true
+type reporter struct{}
+
+func New() (*reporter, error) {
+	key := ""
+	envir := "production"
+
+	err := errors.Join(
+		env.String(&key, IMGPROXY_HONEYBADGER_KEY),
+		env.String(&envir, IMGPROXY_HONEYBADGER_ENV),
+	)
+	if err != nil {
+		return nil, err
 	}
-}
 
-func Report(err error, req *http.Request, meta map[string]any) {
-	if !enabled {
-		return
+	if len(key) == 0 {
+		return nil, nil
 	}
 
+	honeybadger.Configure(honeybadger.Configuration{
+		APIKey: key,
+		Env:    envir,
+	})
+
+	return &reporter{}, nil
+}
+
+func (r *reporter) Report(err error, req *http.Request, meta map[string]any) {
 	extra := make(honeybadger.CGIData, len(req.Header)+len(meta))
 
 	for k, v := range req.Header {
@@ -52,3 +66,7 @@ func Report(err error, req *http.Request, meta map[string]any) {
 
 	honeybadger.Notify(hbErr, req.URL, extra)
 }
+
+func (r *reporter) Close() {
+	// noop
+}

+ 44 - 17
errorreport/sentry/sentry.go

@@ -1,37 +1,60 @@
 package sentry
 
 import (
+	"errors"
+	"fmt"
 	"net/http"
 	"time"
 
 	"github.com/getsentry/sentry-go"
 
-	"github.com/imgproxy/imgproxy/v3/config"
+	"github.com/imgproxy/imgproxy/v3/env"
+	"github.com/imgproxy/imgproxy/v3/version"
 )
 
-var (
-	enabled bool
+const (
+	// flushTimeout is the maximum time to wait for Sentry to send events
+	flushTimeout = 5 * time.Second
+)
 
-	timeout = 5 * time.Second
+var (
+	IMGPROXY_SENTRY_DSN         = env.Describe("IMGPROXY_SENTRY_DSN", "string")
+	IMGPROXY_SENTRY_RELEASE     = env.Describe("IMGPROXY_SENTRY_RELEASE", "string")
+	IMGPROXY_SENTRY_ENVIRONMENT = env.Describe("IMGPROXY_SENTRY_ENVIRONMENT", "string")
 )
 
-func Init() {
-	if len(config.SentryDSN) > 0 {
-		sentry.Init(sentry.ClientOptions{
-			Dsn:         config.SentryDSN,
-			Release:     config.SentryRelease,
-			Environment: config.SentryEnvironment,
-		})
+// reporter is a Sentry error reporter
+type reporter struct{}
 
-		enabled = true
+// new creates and configures a new Sentry reporter
+func New() (*reporter, error) {
+	dsn := ""
+	environment := "production"
+	release := fmt.Sprintf("imgproxy@%s", version.Version)
+
+	err := errors.Join(
+		env.String(&dsn, IMGPROXY_SENTRY_DSN),
+		env.String(&release, IMGPROXY_SENTRY_RELEASE),
+		env.String(&environment, IMGPROXY_SENTRY_ENVIRONMENT),
+	)
+	if err != nil {
+		return nil, err
 	}
-}
 
-func Report(err error, req *http.Request, meta map[string]any) {
-	if !enabled {
-		return
+	if len(dsn) == 0 {
+		return nil, nil
 	}
 
+	sentry.Init(sentry.ClientOptions{
+		Dsn:         dsn,
+		Release:     release,
+		Environment: environment,
+	})
+
+	return &reporter{}, nil
+}
+
+func (r *reporter) Report(err error, req *http.Request, meta map[string]any) {
 	hub := sentry.CurrentHub().Clone()
 	hub.Scope().SetRequest(req)
 	hub.Scope().SetLevel(sentry.LevelError)
@@ -55,7 +78,11 @@ func Report(err error, req *http.Request, meta map[string]any) {
 
 		eventID := hub.CaptureEvent(event)
 		if eventID != nil {
-			hub.Flush(timeout)
+			hub.Flush(flushTimeout)
 		}
 	}
 }
+
+func (r *reporter) Close() {
+	sentry.Flush(flushTimeout)
+}

+ 4 - 2
init.go

@@ -58,7 +58,9 @@ func Init() error {
 		return err
 	}
 
-	errorreport.Init()
+	if err := errorreport.Init(); err != nil {
+		return err
+	}
 
 	return nil
 }
@@ -66,6 +68,6 @@ func Init() error {
 // Shutdown performs global cleanup
 func Shutdown() {
 	monitoring.Stop()
-	errorreport.Close()
 	vips.Shutdown()
+	errorreport.Close()
 }

+ 48 - 13
server/responsewriter/config.go

@@ -5,7 +5,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/ensure"
 	"github.com/imgproxy/imgproxy/v3/env"
 )
@@ -16,6 +15,16 @@ var (
 	IMGPROXY_FALLBACK_IMAGE_TTL        = env.Describe("IMGPROXY_FALLBACK_IMAGE_TTL", "seconds >= 0")
 	IMGPROXY_CACHE_CONTROL_PASSTHROUGH = env.Describe("IMGPROXY_CACHE_CONTROL_PASSTHROUGH", "boolean")
 	IMGPROXY_WRITE_RESPONSE_TIMEOUT    = env.Describe("IMGPROXY_WRITE_RESPONSE_TIMEOUT", "seconds > 0")
+
+	// NOTE: These are referenced here to determine if we need to set the Vary header
+	// Unfotunately, we can not reuse them from optionsparser package due to import cycle
+	IMGPROXY_AUTO_WEBP           = env.Describe("IMGPROXY_AUTO_WEBP", "boolean")
+	IMGPROXY_AUTO_AVIF           = env.Describe("IMGPROXY_AUTO_AVIF", "boolean")
+	IMGPROXY_AUTO_JXL            = env.Describe("IMGPROXY_AUTO_JXL", "boolean")
+	IMGPROXY_ENFORCE_WEBP        = env.Describe("IMGPROXY_ENFORCE_WEBP", "boolean")
+	IMGPROXY_ENFORCE_AVIF        = env.Describe("IMGPROXY_ENFORCE_AVIF", "boolean")
+	IMGPROXY_ENFORCE_JXL         = env.Describe("IMGPROXY_ENFORCE_JXL", "boolean")
+	IMGPROXY_ENABLE_CLIENT_HINTS = env.Describe("IMGPROXY_ENABLE_CLIENT_HINTS", "boolean")
 )
 
 // Config holds configuration for response writer
@@ -57,11 +66,19 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
 
 	vary := make([]string, 0)
 
-	if c.envEnableFormatDetection() {
+	var ok bool
+
+	if err, ok = c.envEnableFormatDetection(); err != nil {
+		return nil, err
+	}
+	if ok {
 		vary = append(vary, "Accept")
 	}
 
-	if c.envEnableClientHints() {
+	if err, ok = c.envEnableClientHints(); err != nil {
+		return nil, err
+	}
+	if ok {
 		vary = append(vary, "Sec-CH-DPR", "DPR", "Sec-CH-Width", "Width")
 	}
 
@@ -70,18 +87,36 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
 	return c, nil
 }
 
-// TODO: REMOVE REFERENCE TO GLOBAL CONFIG
-func (c *Config) envEnableFormatDetection() bool {
-	return config.AutoWebp ||
-		config.EnforceWebp ||
-		config.AutoAvif ||
-		config.EnforceAvif ||
-		config.AutoJxl ||
-		config.EnforceJxl
+// envEnableFormatDetection checks if any of the format detection options are enabled
+func (c *Config) envEnableFormatDetection() (error, bool) {
+	var autoWebp, enforceWebp, autoAvif, enforceAvif, autoJxl, enforceJxl bool
+
+	// We won't need those variables in runtime, hence, we could
+	// read them here once into local variables
+	err := errors.Join(
+		env.Bool(&autoWebp, IMGPROXY_AUTO_WEBP),
+		env.Bool(&enforceWebp, IMGPROXY_ENFORCE_WEBP),
+		env.Bool(&autoAvif, IMGPROXY_AUTO_AVIF),
+		env.Bool(&enforceAvif, IMGPROXY_ENFORCE_AVIF),
+		env.Bool(&autoJxl, IMGPROXY_AUTO_JXL),
+		env.Bool(&enforceJxl, IMGPROXY_ENFORCE_JXL),
+	)
+	if err != nil {
+		return err, false
+	}
+
+	return nil, autoWebp ||
+		enforceWebp ||
+		autoAvif ||
+		enforceAvif ||
+		autoJxl ||
+		enforceJxl
 }
 
-func (c *Config) envEnableClientHints() bool {
-	return config.EnableClientHints
+// envEnableClientHints checks if client hints are enabled
+func (c *Config) envEnableClientHints() (err error, ok bool) {
+	err = env.Bool(&ok, IMGPROXY_ENABLE_CLIENT_HINTS)
+	return
 }
 
 // Validate checks config for errors

+ 1 - 0
server/server_test.go

@@ -24,6 +24,7 @@ func (s *ServerTestSuite) SetupTest() {
 
 	s.config = &c
 	s.config.Bind = "127.0.0.1:0" // Use port 0 for auto-assignment
+
 	r, err := NewRouter(s.config)
 	s.Require().NoError(err)
 	s.blankRouter = r