summaryrefslogtreecommitdiff
path: root/analytics/analytics.go
diff options
context:
space:
mode:
Diffstat (limited to 'analytics/analytics.go')
-rw-r--r--analytics/analytics.go117
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)
+}