feat: add metrics system, move webhook feature to dedicated module (#372)

This commit is contained in:
Julien Neuhart
2021-10-19 18:35:24 +02:00
committed by GitHub
parent b839069af1
commit d4c47cecf2
39 changed files with 3359 additions and 1309 deletions
+139
View File
@@ -0,0 +1,139 @@
package webhook
import (
"fmt"
"io"
"strconv"
"time"
"github.com/hashicorp/go-retryablehttp"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
)
// client gathers all the data required to send a request to a webhook.
type client struct {
url string
method string
errorURL string
errorMethod string
extraHTTPHeaders map[string]string
startTime time.Time
client *retryablehttp.Client
logger *zap.Logger
}
// send call the webhook either to send the success response or the error response.
func (c client) send(body io.Reader, headers map[string]string, erroed bool) error {
URL := c.url
if erroed {
URL = c.errorURL
}
method := c.method
if erroed {
method = c.errorMethod
}
req, err := retryablehttp.NewRequest(method, URL, body)
if err != nil {
return fmt.Errorf("create '%s' request to '%s': %w", method, URL, err)
}
req.Header.Set("User-Agent", "Gotenberg")
// Extra HTTP headers are the custom headers from the user.
for key, value := range c.extraHTTPHeaders {
req.Header.Set(key, value)
}
// Middleware caller's headers > extra HTTP headers from the user.
contentLength, ok := headers[echo.HeaderContentLength]
if ok {
// Golang "http" package should automatically calculate the size of the
// body. But, when using a buffered file reader, it does not work.
// Worse, the "Content-Length" header is also removed. Therefore, in
// order to keep this valuable information, we have to trust the caller
// by reading the value of the "Content-Length" entry and set it as the
// content length of the request. It's kinda sub-optimal, but hey, at
// least it works.
bodySize, err := strconv.ParseInt(contentLength, 10, 64)
if err != nil {
return fmt.Errorf("parse content length entry: %w", err)
}
req.ContentLength = bodySize
}
for key, value := range headers {
req.Header.Set(key, value)
}
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("send '%s' request to '%s': %w", method, URL, err)
}
defer func() {
err := resp.Body.Close()
if err != nil {
c.logger.Error(fmt.Sprintf("close response body from '%s': %s", URL, err))
}
}()
// Last piece for calculating the latency.
finishTime := time.Now()
// Now let's log!
fields := make([]zap.Field, 5)
fields[0] = zap.String("webhook_url", URL)
fields[1] = zap.String("method", method)
fields[2] = zap.Int64("latency", int64(finishTime.Sub(c.startTime)))
fields[3] = zap.String("latency_human", finishTime.Sub(c.startTime).String())
fields[4] = zap.Int64("bytes_out", req.ContentLength)
if erroed {
c.logger.Warn("request to webhook with error details handled", fields...)
return nil
}
c.logger.Info("request to webhook handled", fields...)
return nil
}
// leveledLogger is wrapper around a zap.Logger which is used by the
// retryablehttp.Client.
type leveledLogger struct {
logger *zap.Logger
}
// Error logs a message at error level using the wrapped zap.Logger.
func (leveled leveledLogger) Error(msg string, keysAndValues ...interface{}) {
leveled.logger.Error(fmt.Sprintf("%s: %+v", msg, keysAndValues))
}
// Warn logs a message at warning level using the wrapped zap.Logger.
func (leveled leveledLogger) Warn(msg string, keysAndValues ...interface{}) {
leveled.logger.Warn(fmt.Sprintf("%s: %+v", msg, keysAndValues))
}
// Info logs a message at info level using the wrapped zap.Logger.
func (leveled leveledLogger) Info(msg string, keysAndValues ...interface{}) {
leveled.logger.Info(fmt.Sprintf("%s: %+v", msg, keysAndValues))
}
// Debug logs a message at debug level using the wrapped zap.Logger.
func (leveled leveledLogger) Debug(msg string, keysAndValues ...interface{}) {
leveled.logger.Debug(fmt.Sprintf("%s: %+v", msg, keysAndValues))
}
// Interface guards.
var (
_ retryablehttp.LeveledLogger = (*leveledLogger)(nil)
)