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