diff options
Diffstat (limited to 'analytics/analytics.go')
-rw-r--r-- | analytics/analytics.go | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/analytics/analytics.go b/analytics/analytics.go new file mode 100644 index 0000000..16d12c5 --- /dev/null +++ b/analytics/analytics.go @@ -0,0 +1,117 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net" + "net/http" + "net/http/fcgi" + "time" + + "blitiri.com.ar/go/systemd" + "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/pgxpool" +) + +type Event struct { + Created_ts int64 + Kind string + Details string +} + +func getListener() (net.Listener, error) { + listeners, err := systemd.Listeners() + if err != nil { + return nil, fmt.Errorf("unable to get systemd listeners: %w", err) + } + count := 0 + for _, listeners := range listeners { + count += len(listeners) + if count == 1 { + return listeners[0], nil + } + } + return nil, fmt.Errorf("expected 1 systemd fd passed, got %v", count) +} + +func httpEnd(w http.ResponseWriter, r *http.Request, content string, code int) { + log.Printf("%v %d %d %s", r.RemoteAddr, r.ContentLength, code, content) + http.Error(w, content, code) +} + +func main() { + conn, err := pgxpool.Connect(context.Background(), "") + if err != nil { + log.Fatal("Unable to connect to database: ", err) + } + defer conn.Close() + + http.HandleFunc("/analytics", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + if url, ok := r.URL.Query()["url"]; ok { + if len(url) > 1 { + httpEnd(w, r, "too many urls", 400) + } else { + http.Redirect(w, r, url[0], 303) + _, err = conn.Exec(context.Background(), "insert into events(created_ts, ip_addr, kind, details) values ($1, $2, $3, $4)", time.Now(), r.RemoteAddr, "r", url[0]) + if err != nil { + log.Printf("%v 0 303 db error: %s", r.RemoteAddr, err) + return + } + log.Printf("%v 0 303 ok", r.RemoteAddr) + } + } else { + httpEnd(w, r, "missing url for GET", 400) + } + return + } + if r.Method != "POST" { + httpEnd(w, r, "method not allowed: "+r.Method, 405) + return + } + if r.ContentLength <= 0 { + httpEnd(w, r, "length required", 411) + return + } + if r.ContentLength > 1048576 { + httpEnd(w, r, "too much data", 413) + return + } + + data, err := io.ReadAll(r.Body) + if err != nil { + httpEnd(w, r, "read error", 400) + return + } + var events []Event + if err := json.Unmarshal(data, &events); err != nil { + log.Print(err) + httpEnd(w, r, "bad json", 400) + return + } + + _, err = conn.CopyFrom( + r.Context(), + pgx.Identifier{"events"}, + []string{"created_ts", "ip_addr", "kind", "details"}, + pgx.CopyFromSlice(len(events), func(i int) ([]interface{}, error) { + return []interface{}{time.Unix(events[i].Created_ts/1000, events[i].Created_ts%1000*1000000), r.RemoteAddr, events[i].Kind, events[i].Details}, nil + }), + ) + if err != nil { + log.Printf("%v %d 500 db error: %s", r.RemoteAddr, r.ContentLength, err) + http.Error(w, "db error", 500) + } else { + httpEnd(w, r, "ok", 200) + } + }) + + l, err := getListener() + if err != nil { + log.Fatal(err) + } + fcgi.Serve(l, nil) +} |