From a978d4921d9665b15db033d0ad05fee452ea7e66 Mon Sep 17 00:00:00 2001 From: Oleh Astappiev Date: Sun, 8 Dec 2024 21:46:35 +0100 Subject: [PATCH] feat: add `responseWrapper` to exclude errors (status codes >= 400) from being reported --- readme.md | 2 ++ responsewriter.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ umami.go | 36 +++++++++++++++++++++++++++++++----- umami_worker.go | 2 +- 4 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 responsewriter.go diff --git a/readme.md b/readme.md index 172e2c6..8779a56 100644 --- a/readme.md +++ b/readme.md @@ -86,6 +86,7 @@ entryPoints: |---------------------|-------------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------| | `disabled` | false | `bool` | Set to `true` to disable the plugin | | `debug` | false | `bool` | Something doesn't work? Set to `true` to see more logs (plugins doesn't have access to Traefik's log level) | +| `queueSize` | 1000 | `int` | The maximum number of events that can be queued before they are sent to the Umami server | | `umamiHost` | - | `string` | Umami server url, reachable from within traefik (container), e.g. `http://umami:3000` | | `umamiToken` | - | `string` | An API Token, used to automatize work with websites, not needed if you provide `websites` | | `umamiUsername` | - | `string` | An alternative to `umamiToken`, you can provide an username and password | @@ -93,6 +94,7 @@ entryPoints: | `umamiTeamId` | - | `string` | In order to organize websites, you can use Umami Teams | | `websites` | - | `map` | A map of hostnames and their associated Umami IDs. Can also be used to override or extend fetched websites | | `createNewWebsites` | false | `bool` | If set to `true`, will try to create a new website on Umami, if domain not found there | +| `trackErrors` | false | `bool` | If set to `true`, will track errors (status codes >= 400) | | `trackAllResources` | false | `bool` | Defines whether all requests for any resource should be tracked. By default, only requests that are believed to contain content are tracked | | `trackExtensions` | | `string[]` | Defines an alternative list of file extensions that should be tracked | | `ignoreUserAgents` | | `string[]` | A list of user agents that should be ignored from tracking, e.g. `["Googlebot", "Uptime-Kuma"]` (matched with `strings.Contains`) | diff --git a/responsewriter.go b/responsewriter.go new file mode 100644 index 0000000..82505db --- /dev/null +++ b/responsewriter.go @@ -0,0 +1,44 @@ +package traefik_umami_feeder + +import ( + "bufio" + "fmt" + "net" + "net/http" +) + +// Copied and adapted from https://github.com/safing/plausiblefeeder/blob/master/responsewriter.go +// Licensed as MIT license + +// ResponseWriter is used to wrap given response writers. +type ResponseWriter struct { + http.ResponseWriter + + request *http.Request + feeder *UmamiFeeder +} + +// WriteHeader adds custom handling to the wrapped WriterHeader method. +func (rw *ResponseWriter) WriteHeader(code int) { + if rw.feeder.shouldTrackStatus(code) { + rw.feeder.submitToFeed(rw.request, code) + } + + // Continue with the original method. + rw.ResponseWriter.WriteHeader(code) +} + +func (rw *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hijacker, ok := rw.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, fmt.Errorf("%T is not a http.Hijacker", rw.ResponseWriter) + } + + return hijacker.Hijack() +} + +func (rw *ResponseWriter) Flush() { + if flusher, ok := rw.ResponseWriter.(http.Flusher); ok { + flusher.Flush() + } +} diff --git a/umami.go b/umami.go index 3a92486..2cafacb 100644 --- a/umami.go +++ b/umami.go @@ -20,7 +20,7 @@ type Config struct { // Debug enables debug logging, be prepared for flooding. Debug bool `json:"debug"` // QueueSize defines the size of queue, i.e. the amount of events that are waiting to be submitted to Umami. - QueueSize int + QueueSize int `json:"queueSize"` // UmamiHost is the URL of the Umami instance. UmamiHost string `json:"umamiHost"` @@ -39,6 +39,8 @@ type Config struct { // CreateNewWebsites when set to true, the plugin will create new websites using API, UmamiToken is required. CreateNewWebsites bool `json:"createNewWebsites"` + // TrackErrors defines whether errors (status codes >= 400) should be tracked. + TrackErrors bool `json:"trackErrors"` // TrackAllResources defines whether all requests for any resource should be tracked. // By default, only requests that are believed to contain content are tracked. TrackAllResources bool `json:"trackAllResources"` @@ -58,9 +60,10 @@ type Config struct { // CreateConfig creates the default plugin configuration. func CreateConfig() *Config { return &Config{ - Disabled: false, - Debug: false, - QueueSize: 1000, + Disabled: false, + Debug: false, + QueueSize: 1000, + TrackErrors: false, UmamiHost: "", UmamiToken: "", @@ -96,6 +99,7 @@ type UmamiFeeder struct { websites map[string]string createNewWebsites bool + trackErrors bool trackAllResources bool trackExtensions []string @@ -123,6 +127,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h websites: config.Websites, createNewWebsites: config.CreateNewWebsites, + trackErrors: config.TrackErrors, trackAllResources: config.TrackAllResources, trackExtensions: config.TrackExtensions, @@ -225,7 +230,16 @@ func (h *UmamiFeeder) verifyConfig(config *Config) error { func (h *UmamiFeeder) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if !h.isDisabled && h.shouldTrack(req) { - h.submitToFeed(req) + // If the resource should be reported, we wrap the response writer and check the status code before reporting + wrappedResponseWriter := &ResponseWriter{ + ResponseWriter: rw, + request: req, + feeder: h, + } + + // Continue with next handler. + h.next.ServeHTTP(wrappedResponseWriter, req) + return } h.next.ServeHTTP(rw, req) @@ -316,6 +330,18 @@ func (h *UmamiFeeder) shouldTrackResource(url string) bool { return false } +func (h *UmamiFeeder) shouldTrackStatus(statusCode int) (report bool) { + if statusCode >= 400 { + if h.trackErrors { + return true + } + + h.debug("not reporting %d error", statusCode) + return false + } + return true +} + func (h *UmamiFeeder) error(message string) { if h.logHandler != nil { now := time.Now().Format("2006-01-02T15:04:05Z") diff --git a/umami_worker.go b/umami_worker.go index b93377b..d7bbbc5 100644 --- a/umami_worker.go +++ b/umami_worker.go @@ -46,7 +46,7 @@ var headersToCopy = []string{ // Copied and adapted from https://github.com/safing/plausiblefeeder/blob/master/event.go // Licensed as MIT license -func (h *UmamiFeeder) submitToFeed(req *http.Request) { +func (h *UmamiFeeder) submitToFeed(req *http.Request, code int) { body := SendBody{ Payload: SendPayload{ Hostname: parseDomainFromHost(req.Host),