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))
}
|