feat: add responseWrapper to exclude errors (status codes >= 400) from being reported

This commit is contained in:
Oleh Astappiev 2024-12-08 21:46:35 +01:00
parent 02f9dd951c
commit a978d4921d
No known key found for this signature in database
GPG key ID: 37D7AC22589E9632
4 changed files with 78 additions and 6 deletions

View file

@ -86,6 +86,7 @@ entryPoints:
|---------------------|-------------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------| |---------------------|-------------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| `disabled` | false | `bool` | Set to `true` to disable the plugin | | `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) | | `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` | | `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` | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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`) | | `ignoreUserAgents` | | `string[]` | A list of user agents that should be ignored from tracking, e.g. `["Googlebot", "Uptime-Kuma"]` (matched with `strings.Contains`) |

44
responsewriter.go Normal file
View file

@ -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()
}
}

View file

@ -20,7 +20,7 @@ type Config struct {
// Debug enables debug logging, be prepared for flooding. // Debug enables debug logging, be prepared for flooding.
Debug bool `json:"debug"` 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 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 is the URL of the Umami instance.
UmamiHost string `json:"umamiHost"` 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 when set to true, the plugin will create new websites using API, UmamiToken is required.
CreateNewWebsites bool `json:"createNewWebsites"` 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. // TrackAllResources defines whether all requests for any resource should be tracked.
// By default, only requests that are believed to contain content are tracked. // By default, only requests that are believed to contain content are tracked.
TrackAllResources bool `json:"trackAllResources"` TrackAllResources bool `json:"trackAllResources"`
@ -61,6 +63,7 @@ func CreateConfig() *Config {
Disabled: false, Disabled: false,
Debug: false, Debug: false,
QueueSize: 1000, QueueSize: 1000,
TrackErrors: false,
UmamiHost: "", UmamiHost: "",
UmamiToken: "", UmamiToken: "",
@ -96,6 +99,7 @@ type UmamiFeeder struct {
websites map[string]string websites map[string]string
createNewWebsites bool createNewWebsites bool
trackErrors bool
trackAllResources bool trackAllResources bool
trackExtensions []string trackExtensions []string
@ -123,6 +127,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
websites: config.Websites, websites: config.Websites,
createNewWebsites: config.CreateNewWebsites, createNewWebsites: config.CreateNewWebsites,
trackErrors: config.TrackErrors,
trackAllResources: config.TrackAllResources, trackAllResources: config.TrackAllResources,
trackExtensions: config.TrackExtensions, trackExtensions: config.TrackExtensions,
@ -225,7 +230,16 @@ func (h *UmamiFeeder) verifyConfig(config *Config) error {
func (h *UmamiFeeder) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (h *UmamiFeeder) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if !h.isDisabled && h.shouldTrack(req) { 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) h.next.ServeHTTP(rw, req)
@ -316,6 +330,18 @@ func (h *UmamiFeeder) shouldTrackResource(url string) bool {
return false 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) { func (h *UmamiFeeder) error(message string) {
if h.logHandler != nil { if h.logHandler != nil {
now := time.Now().Format("2006-01-02T15:04:05Z") now := time.Now().Format("2006-01-02T15:04:05Z")

View file

@ -46,7 +46,7 @@ var headersToCopy = []string{
// Copied and adapted from https://github.com/safing/plausiblefeeder/blob/master/event.go // Copied and adapted from https://github.com/safing/plausiblefeeder/blob/master/event.go
// Licensed as MIT license // Licensed as MIT license
func (h *UmamiFeeder) submitToFeed(req *http.Request) { func (h *UmamiFeeder) submitToFeed(req *http.Request, code int) {
body := SendBody{ body := SendBody{
Payload: SendPayload{ Payload: SendPayload{
Hostname: parseDomainFromHost(req.Host), Hostname: parseDomainFromHost(req.Host),