internal/stats/goatcounter/count.go (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | package goatcounter import ( "bytes" "context" "encoding/json" "io" "net/http" "strings" "time" "gitlab.com/tozd/go/errors" "go.alanpearce.eu/homestead/internal/config" "go.alanpearce.eu/x/log" ) const timeout = 1 * time.Second type Options struct { URL *config.URL Logger *log.Logger Token string } type URLs struct { count string } type Goatcounter struct { log *log.Logger token string client *http.Client urls URLs } type hit struct { IP string `json:"ip"` Path string `json:"path"` Query string `json:"query"` Referrer string `json:"ref"` UserAgent string `json:"user_agent"` } type countBody struct { NoSessions bool `json:"no_sessions"` Hits []hit `json:"hits"` } func New(options *Options) *Goatcounter { baseURL := options.URL if strings.HasSuffix(baseURL.Path, "/count") { baseURL.Path = "/api/v0/" } return &Goatcounter{ log: options.Logger, token: options.Token, client: &http.Client{ Timeout: 5 * time.Second, }, urls: URLs{ count: baseURL.JoinPath("count").String(), }, } } func (gc *Goatcounter) Count(r *http.Request) { err := gc.count(r) if err != nil { gc.log.Warn("could not log page view", "error", err) } } func (gc *Goatcounter) count(userReq *http.Request) error { body, err := json.Marshal(&countBody{ NoSessions: true, Hits: []hit{ { IP: userReq.RemoteAddr, Path: userReq.URL.Path, Query: userReq.URL.RawQuery, Referrer: userReq.Header.Get("Referer"), UserAgent: userReq.Header.Get("User-Agent"), }, }, }) if err != nil { return errors.WithMessage(err, "could not marshal JSON") } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() req, err := http.NewRequestWithContext( ctx, http.MethodPost, gc.urls.count, bytes.NewBuffer(body), ) if err != nil { return errors.WithMessage(err, "could not create HTTP request") } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+gc.token) go func(req *http.Request) { var body []byte res, err := gc.client.Do(req) if err != nil { gc.log.Warn("could not perform HTTP request", "error", err) } if res.Body != nil { defer res.Body.Close() body, err = io.ReadAll(res.Body) if err != nil { gc.log.Warn("could not read error body", "error", err) } } if res.StatusCode != http.StatusAccepted { gc.log.Warn("failed to log pageview", "status", res.StatusCode, "body", body) } }(req) return nil } |