summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--files/glt/stream-stats.go190
-rw-r--r--spreadspace/glt-stream.yml63
2 files changed, 253 insertions, 0 deletions
diff --git a/files/glt/stream-stats.go b/files/glt/stream-stats.go
new file mode 100644
index 00000000..5dec4e8f
--- /dev/null
+++ b/files/glt/stream-stats.go
@@ -0,0 +1,190 @@
+package main
+
+import (
+ "crypto/sha256"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "strconv"
+ "sync"
+ "time"
+)
+
+type LatestRequests map[string]bool
+
+var last5min LatestRequests
+var lMutex = &sync.Mutex{}
+
+const dateFormat = time.RFC3339
+
+func init() {
+ last5min = make(LatestRequests)
+}
+
+// find the next timestamp (i.e. time when minute is 4 mod 5 and second is 0)
+func nextTimestamp() time.Time {
+ now := time.Now()
+ if now.Minute()%5 == 4 && now.Second() == 0 {
+ return now.Add(5 * 60 * time.Second)
+ }
+
+ minDiff := 5
+ switch now.Minute() % 5 {
+ case 0:
+ minDiff = 4
+ case 1:
+ minDiff = 3
+ case 2:
+ minDiff = 2
+ case 3:
+ minDiff = 1
+ case 4:
+ minDiff = 5
+ }
+ return now.Add(-time.Duration(now.Second()) * time.Second).Add(time.Duration(minDiff) * 60 * time.Second)
+}
+
+// find the previous timestamp
+func previousTimestamp() time.Time {
+ now := time.Now()
+ if now.Minute()%5 == 4 && now.Second() == 0 {
+ return now.Add(-5 * 60 * time.Second)
+ }
+
+ minDiff := (now.Minute() % 5) + 1
+ return now.Add(-time.Duration(now.Second()) * time.Second).Add(-time.Duration(minDiff) * 60 * time.Second)
+}
+
+// writeToFile writes the 5min result to the file by appending data
+func writeToFile() {
+ filePath := os.Args[2]
+ timestamp := time.Now().Add(-5 * 60 * time.Second)
+ db := make(map[string]uint32)
+
+ // collect new count and erase data from 5-minutes data structure
+ lMutex.Lock()
+ latestCount := len(last5min)
+ last5min = make(LatestRequests)
+ lMutex.Unlock()
+
+ // read in existing data
+ content, err := ioutil.ReadFile(filePath)
+ if err == nil {
+ srcData := make(map[string]uint32)
+ err = json.Unmarshal(content, &srcData)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to unmarshal file '%s': %s\n", filePath, err.Error())
+ return
+ }
+
+ // copy data over to database
+ for k, v := range srcData {
+ db[k] = v
+ }
+ }
+
+ // update database with latest count
+ db[timestamp.Format(dateFormat)] = uint32(latestCount)
+
+ // write to file
+ dump, _ := json.MarshalIndent(db, "", " ")
+ err = ioutil.WriteFile(filePath, dump, 0644)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error while writing file '%s': %s\n", filePath, err.Error())
+ }
+}
+
+// handle a request to /
+func handle(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-type", "text/plain; charset=utf-8")
+ _, err := w.Write([]byte("request counter\nby meisterluk\nroutes: {/req, /list}\n"))
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+}
+
+// handle a request to /req
+// increments the counter within 5min plus one unless this client was already registered
+func handleRequest(w http.ResponseWriter, r *http.Request) {
+ // generate a key which detects trivial double requests
+ var ident string
+ ident = r.Header.Get("User-Agent")
+ if r.Header.Get("X-FORWARDED-FOR") != "" {
+ ident += r.Header.Get("X-FORWARDED-FOR")
+ }
+ if r.RemoteAddr != "" {
+ ident += r.RemoteAddr
+ }
+ //ident += time.Now().Format(dateFormat) // add this line to register every request for debugging
+ h := sha256.New()
+ key := string(h.Sum([]byte(ident)))
+
+ // register this client for counting
+ lMutex.Lock()
+ last5min[key] = true
+ defer lMutex.Unlock()
+
+ w.Write([]byte("request registered\n"))
+}
+
+func handleList(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-type", "text/plain; charset=utf-8")
+ srcFile := os.Args[2]
+ db := make(map[string]uint32)
+
+ // add entry for current data
+ db[previousTimestamp().Format(dateFormat)] = uint32(len(last5min))
+
+ // read file
+ for {
+ content, err := ioutil.ReadFile(srcFile)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ break
+ }
+ srcDB := make(map[string]uint32)
+ err = json.Unmarshal(content, &srcDB)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ break
+ }
+
+ // copy data into db
+ for k, v := range srcDB {
+ db[k] = v
+ }
+ break // for loop used only for control flow
+ }
+
+ // print data
+ for timestamp, count := range db {
+ w.Write([]byte(timestamp + "\t" + strconv.Itoa(int(count)) + "\n"))
+ }
+}
+
+func main() {
+ if len(os.Args) != 3 {
+ fmt.Fprintln(os.Stderr, "usage: ./req-counter <int:port> <str:data-filepath>")
+ os.Exit(1)
+ }
+
+ http.HandleFunc("/", handle)
+ http.HandleFunc("/req", handleRequest)
+ http.HandleFunc("/list", handleList)
+
+ go func() {
+ for {
+ now := time.Now()
+ time.Sleep(nextTimestamp().Sub(now))
+
+ writeToFile()
+ fmt.Fprintf(os.Stderr, "File '%s' written.\n", os.Args[2])
+ }
+ }()
+
+ fmt.Println("listening on " + os.Args[1])
+ log.Fatal(http.ListenAndServe(os.Args[1], nil))
+}
diff --git a/spreadspace/glt-stream.yml b/spreadspace/glt-stream.yml
index 355c989f..48541d85 100644
--- a/spreadspace/glt-stream.yml
+++ b/spreadspace/glt-stream.yml
@@ -46,3 +46,66 @@
autoindex: {}
include_role:
name: nginx/vhost
+
+ - name: install golang
+ apt:
+ name: go
+ state: present
+
+ - name: create base directory for stats
+ file:
+ path: /srv/www/stats
+ state: directory
+
+ - name: add user for stats
+ user:
+ name: stats
+ system: yes
+ home: /srv/www/stats
+
+ - name: create data and gocache directories for stats
+ loop:
+ - data
+ - .gocache
+ file:
+ path: "/srv/www/stats/{{ item }}"
+ state: directory
+ group: stats
+ mode: 0775
+
+ - name: install stats collector script
+ copy:
+ src: "{{ global_files_dir }}/glt/stream-stats.go"
+ dest: /srv/www/stats/stream-stats.go
+
+ - name: install systemd unit for stats collector
+ copy:
+ content: |
+ [Unit]
+ Description=GLT21 Stream Stats Collector
+
+ [Service]
+ Type=simple
+ Environment="GOCACHE=/srv/www/stats/.gocache"
+ ExecStart=/usr/bin/go run /srv/www/stats/stream-stats.go 127.0.0.1:4200 /srv/www/stats/data/glt21.json
+ NoNewPrivileges=yes
+ PrivateTmp=yes
+ PrivateDevices=yes
+ ProtectSystem=strict
+ ReadWritePaths=/srv/www/stats/data /srv/www/stats/.gocache
+ ProtectHome=yes
+ ProtectKernelTunables=yes
+ ProtectControlGroups=yes
+ RestrictRealtime=yes
+ RestrictAddressFamilies=AF_INET
+
+ [Install]
+ WantedBy=multi-user.target
+ dest: /etc/systemd/system/stream-stats.service
+
+ - name: make sure stats collector service unit is enabled and started
+ systemd:
+ name: stream-stats.service
+ daemon_reload: yes
+ enabled: yes
+ state: started