Better error metrics

This commit is contained in:
DarthSim
2022-07-20 15:49:05 +06:00
parent 1524a0e973
commit 20039f49a5
8 changed files with 95 additions and 73 deletions

View File

@@ -2,16 +2,15 @@ package datadog
import ( import (
"context" "context"
"errors"
"net/http" "net/http"
"os" "os"
"time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"github.com/imgproxy/imgproxy/v3/config" "github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/metrics/errformat"
"github.com/imgproxy/imgproxy/v3/version" "github.com/imgproxy/imgproxy/v3/version"
) )
@@ -79,24 +78,14 @@ func StartSpan(ctx context.Context, name string) context.CancelFunc {
return func() {} return func() {}
} }
func SendError(ctx context.Context, err error) { func SendError(ctx context.Context, errType string, err error) {
if !enabled { if !enabled {
return return
} }
if rootSpan, ok := ctx.Value(spanCtxKey{}).(tracer.Span); ok { if rootSpan, ok := ctx.Value(spanCtxKey{}).(tracer.Span); ok {
rootSpan.Finish(tracer.WithError(err)) rootSpan.SetTag(ext.Error, err)
} rootSpan.SetTag(ext.ErrorType, errformat.FormatErrType(errType, err))
}
func SendTimeout(ctx context.Context, d time.Duration) {
if !enabled {
return
}
if rootSpan, ok := ctx.Value(spanCtxKey{}).(tracer.Span); ok {
rootSpan.SetTag("timeout_duration", d)
rootSpan.Finish(tracer.WithError(errors.New("Timeout")))
} }
} }

View File

@@ -0,0 +1,21 @@
package errformat
import (
"fmt"
"reflect"
"github.com/sirupsen/logrus"
"github.com/imgproxy/imgproxy/v3/ierrors"
)
func FormatErrType(errType string, err error) string {
errType += "_error"
if _, ok := err.(*ierrors.Error); !ok {
errType = fmt.Sprintf("%s (%s)", errType, reflect.TypeOf(err).String())
logrus.Warnf("ErrType: %s", errType)
}
return errType
}

View File

@@ -3,7 +3,6 @@ package metrics
import ( import (
"context" "context"
"net/http" "net/http"
"time"
"github.com/imgproxy/imgproxy/v3/metrics/datadog" "github.com/imgproxy/imgproxy/v3/metrics/datadog"
"github.com/imgproxy/imgproxy/v3/metrics/newrelic" "github.com/imgproxy/imgproxy/v3/metrics/newrelic"
@@ -76,12 +75,6 @@ func StartProcessingSegment(ctx context.Context) context.CancelFunc {
func SendError(ctx context.Context, errType string, err error) { func SendError(ctx context.Context, errType string, err error) {
prometheus.IncrementErrorsTotal(errType) prometheus.IncrementErrorsTotal(errType)
newrelic.SendError(ctx, err) newrelic.SendError(ctx, errType, err)
datadog.SendError(ctx, err) datadog.SendError(ctx, errType, err)
}
func SendTimeout(ctx context.Context, d time.Duration) {
prometheus.IncrementErrorsTotal("timeout")
newrelic.SendTimeout(ctx, d)
datadog.SendTimeout(ctx, d)
} }

View File

@@ -4,9 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"time"
"github.com/imgproxy/imgproxy/v3/config" "github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/metrics/errformat"
"github.com/newrelic/go-agent/v3/newrelic" "github.com/newrelic/go-agent/v3/newrelic"
) )
@@ -78,28 +78,15 @@ func StartSegment(ctx context.Context, name string) context.CancelFunc {
return func() {} return func() {}
} }
func SendError(ctx context.Context, err error) { func SendError(ctx context.Context, errType string, err error) {
if !enabled {
return
}
if txn, ok := ctx.Value(transactionCtxKey{}).(*newrelic.Transaction); ok {
txn.NoticeError(err)
}
}
func SendTimeout(ctx context.Context, d time.Duration) {
if !enabled { if !enabled {
return return
} }
if txn, ok := ctx.Value(transactionCtxKey{}).(*newrelic.Transaction); ok { if txn, ok := ctx.Value(transactionCtxKey{}).(*newrelic.Transaction); ok {
txn.NoticeError(newrelic.Error{ txn.NoticeError(newrelic.Error{
Message: "Timeout", Message: err.Error(),
Class: "Timeout", Class: errformat.FormatErrType(errType, err),
Attributes: map[string]interface{}{
"time": d.Seconds(),
},
}) })
} }
} }

View File

@@ -53,7 +53,10 @@ func (p pipeline) Run(ctx context.Context, img *vips.Image, po *options.Processi
if err := step(&pctx, img, po, imgdata); err != nil { if err := step(&pctx, img, po, imgdata); err != nil {
return err return err
} }
router.CheckTimeout(ctx)
if err := router.CheckTimeout(ctx); err != nil {
return err
}
} }
return nil return nil

View File

@@ -214,7 +214,9 @@ func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img
} }
imgdata.Close() imgdata.Close()
router.CheckTimeout(ctx) if err := router.CheckTimeout(ctx); err != nil {
return nil, err
}
delta := float64(len(imgdata.Data)) / float64(po.MaxBytes) delta := float64(len(imgdata.Data)) / float64(po.MaxBytes)
switch { switch {

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
@@ -144,6 +145,33 @@ func respondWithNotModified(reqID string, r *http.Request, rw http.ResponseWrite
) )
} }
func sendErrAndPanic(ctx context.Context, errType string, err error) {
send := true
if ierr, ok := err.(*ierrors.Error); ok {
switch ierr.StatusCode {
case http.StatusServiceUnavailable:
errType = "timeout"
case 499:
// Don't need to send a "request cancelled" error
send = false
}
}
if send {
metrics.SendError(ctx, errType, err)
}
panic(err)
}
func checkErr(ctx context.Context, errType string, err error) {
if err == nil {
return
}
sendErrAndPanic(ctx, errType, err)
}
func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) { func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -163,25 +191,29 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
signature = path[:signatureEnd] signature = path[:signatureEnd]
path = path[signatureEnd:] path = path[signatureEnd:]
} else { } else {
panic(ierrors.New(404, fmt.Sprintf("Invalid path: %s", path), "Invalid URL")) sendErrAndPanic(ctx, "path_parsing", ierrors.New(
404, fmt.Sprintf("Invalid path: %s", path), "Invalid URL",
))
} }
if err := security.VerifySignature(signature, path); err != nil { if err := security.VerifySignature(signature, path); err != nil {
panic(ierrors.New(403, err.Error(), "Forbidden")) sendErrAndPanic(ctx, "security", ierrors.New(403, err.Error(), "Forbidden"))
} }
po, imageURL, err := options.ParsePath(path, r.Header) po, imageURL, err := options.ParsePath(path, r.Header)
if err != nil { checkErr(ctx, "path_parsing", err)
panic(err)
}
if !security.VerifySourceURL(imageURL) { if !security.VerifySourceURL(imageURL) {
panic(ierrors.New(404, fmt.Sprintf("Source URL is not allowed: %s", imageURL), "Invalid source")) sendErrAndPanic(ctx, "security", ierrors.New(
404,
fmt.Sprintf("Source URL is not allowed: %s", imageURL),
"Invalid source",
))
} }
// SVG is a special case. Though saving to svg is not supported, SVG->SVG is. // SVG is a special case. Though saving to svg is not supported, SVG->SVG is.
if !vips.SupportsSave(po.Format) && po.Format != imagetype.Unknown && po.Format != imagetype.SVG { if !vips.SupportsSave(po.Format) && po.Format != imagetype.Unknown && po.Format != imagetype.SVG {
panic(ierrors.New( sendErrAndPanic(ctx, "path_parsing", ierrors.New(
422, 422,
fmt.Sprintf("Resulting image format is not supported: %s", po.Format), fmt.Sprintf("Resulting image format is not supported: %s", po.Format),
"Invalid URL", "Invalid URL",
@@ -209,7 +241,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
// We don't actually need to check timeout here, // We don't actually need to check timeout here,
// but it's an easy way to check if this is an actual timeout // but it's an easy way to check if this is an actual timeout
// or the request was cancelled // or the request was cancelled
router.CheckTimeout(ctx) checkErr(ctx, "queue", router.CheckTimeout(ctx))
} }
defer func() { <-processingSem }() defer func() { <-processingSem }()
@@ -221,9 +253,8 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
var cookieJar *cookiejar.Jar var cookieJar *cookiejar.Jar
if config.CookiePassthrough { if config.CookiePassthrough {
if cookieJar, err = cookies.JarFromRequest(r); err != nil { cookieJar, err = cookies.JarFromRequest(r)
panic(err) checkErr(ctx, "download", err)
}
} }
return imagedata.Download(imageURL, "source image", imgRequestHeader, cookieJar) return imagedata.Download(imageURL, "source image", imgRequestHeader, cookieJar)
@@ -258,7 +289,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
originData = imagedata.FallbackImage originData = imagedata.FallbackImage
} }
router.CheckTimeout(ctx) checkErr(ctx, "timeout", router.CheckTimeout(ctx))
if config.ETagEnabled && statusCode == http.StatusOK { if config.ETagEnabled && statusCode == http.StatusOK {
imgDataMatch := etagHandler.SetActualImageData(originData) imgDataMatch := etagHandler.SetActualImageData(originData)
@@ -271,16 +302,14 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
} }
} }
router.CheckTimeout(ctx) checkErr(ctx, "timeout", router.CheckTimeout(ctx))
if originData.Type == po.Format || po.Format == imagetype.Unknown { if originData.Type == po.Format || po.Format == imagetype.Unknown {
// Don't process SVG // Don't process SVG
if originData.Type == imagetype.SVG { if originData.Type == imagetype.SVG {
if config.SanitizeSvg { if config.SanitizeSvg {
sanitized, svgErr := svg.Satitize(originData.Data) sanitized, svgErr := svg.Satitize(originData.Data)
if svgErr != nil { checkErr(ctx, "svg_processing", svgErr)
panic(svgErr)
}
// Since we'll replace origin data, it's better to close it to return // Since we'll replace origin data, it's better to close it to return
// it's buffer to the pool // it's buffer to the pool
@@ -307,7 +336,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
} }
if !vips.SupportsLoad(originData.Type) { if !vips.SupportsLoad(originData.Type) {
panic(ierrors.New( sendErrAndPanic(ctx, "processing", ierrors.New(
422, 422,
fmt.Sprintf("Source image format is not supported: %s", originData.Type), fmt.Sprintf("Source image format is not supported: %s", originData.Type),
"Invalid URL", "Invalid URL",
@@ -316,20 +345,20 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
// At this point we can't allow requested format to be SVG as we can't save SVGs // At this point we can't allow requested format to be SVG as we can't save SVGs
if po.Format == imagetype.SVG { if po.Format == imagetype.SVG {
panic(ierrors.New(422, "Resulting image format is not supported: svg", "Invalid URL")) sendErrAndPanic(ctx, "processing", ierrors.New(
422, "Resulting image format is not supported: svg", "Invalid URL",
))
} }
resultData, err := func() (*imagedata.ImageData, error) { resultData, err := func() (*imagedata.ImageData, error) {
defer metrics.StartProcessingSegment(ctx)() defer metrics.StartProcessingSegment(ctx)()
return processing.ProcessImage(ctx, originData, po) return processing.ProcessImage(ctx, originData, po)
}() }()
if err != nil { checkErr(ctx, "processing", err)
metrics.SendError(ctx, "processing", err)
panic(err)
}
defer resultData.Close() defer resultData.Close()
router.CheckTimeout(ctx) checkErr(ctx, "timeout", router.CheckTimeout(ctx))
respondWithImage(reqID, r, rw, statusCode, resultData, po, imageURL, originData) respondWithImage(reqID, r, rw, statusCode, resultData, po, imageURL, originData)
} }

View File

@@ -8,7 +8,6 @@ import (
"github.com/imgproxy/imgproxy/v3/config" "github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/ierrors" "github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/imgproxy/imgproxy/v3/metrics"
) )
type timerSinceCtxKey = struct{} type timerSinceCtxKey = struct{}
@@ -27,7 +26,7 @@ func ctxTime(ctx context.Context) time.Duration {
return 0 return 0
} }
func CheckTimeout(ctx context.Context) { func CheckTimeout(ctx context.Context) error {
select { select {
case <-ctx.Done(): case <-ctx.Done():
d := ctxTime(ctx) d := ctxTime(ctx)
@@ -35,14 +34,13 @@ func CheckTimeout(ctx context.Context) {
err := ctx.Err() err := ctx.Err()
switch err { switch err {
case context.Canceled: case context.Canceled:
panic(ierrors.New(499, fmt.Sprintf("Request was cancelled after %v", d), "Cancelled")) return ierrors.New(499, fmt.Sprintf("Request was cancelled after %v", d), "Cancelled")
case context.DeadlineExceeded: case context.DeadlineExceeded:
metrics.SendTimeout(ctx, d) return ierrors.New(http.StatusServiceUnavailable, fmt.Sprintf("Request was timed out after %v", d), "Timeout")
panic(ierrors.New(503, fmt.Sprintf("Timeout after %v", d), "Timeout"))
default: default:
panic(err) return err
} }
default: default:
// Go ahead return nil
} }
} }