summaryrefslogtreecommitdiff
path: root/files/glt/stream-stats.go
blob: 6920b513598d0d45bf2126e5f18e6d9877cb3e04 (plain) (blame)
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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")
	ident += r.Header.Get("X-Forwarded-For")
	//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))
}