├── .gitignore ├── README.md ├── bin └── .gitkeep ├── src ├── cdnstats │ ├── analytics.go │ ├── globals.go │ ├── main.go │ ├── stats.go │ ├── updater.go │ └── utils.go ├── sequence │ ├── sequence.go │ └── sequence_test.go └── string_table │ ├── string_table.go │ └── string_table_test.go └── templates └── index.html.template /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | /bin/cdnstats 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CDNStats 2 | 3 | CDNStat is a daemon collecting various statistics from nginx requests: request 4 | count, sent bytes, referer, URI path. 5 | 6 | ## Installation 7 | 8 | ``` 9 | git clone git://github.com/antage/cdnstats 10 | GOPATH=./cdnstats go install cdnstats 11 | cdnstats/bin/cdnstats -h x.x.x.x -p pppp 12 | ``` 13 | 14 | Where x.x.x.x is host name, pppp is port number. 15 | Default values: 127.0.0.1:9090 16 | 17 | Now you can open browser at http://x.x.x.x:pppp/ for web-page displaying 18 | statistics. 19 | 20 | ## How to setup Nginx? 21 | 22 | Configuration example: 23 | 24 | ``` 25 | server { 26 | location / { 27 | root /var/www; 28 | post_action @stats; # after each request send information to cdnstats 29 | } 30 | 31 | location @stats { 32 | proxy_pass http://x.x.x.x:pppp/collect?bucket=[bucket name]&s=[hostname]&uri=$uri; 33 | # don't wait too long 34 | proxy_send_timeout 5s; 35 | proxy_read_timeout 5s; 36 | 37 | # optional header if you use domain name instead of ip-address x.x.x.x 38 | # proxy_set_header Host cdnstat.example.org; 39 | 40 | # this headers are used by cdnstats 41 | proxy_set_header X-Bytes-Sent $body_bytes_sent; 42 | # Referer header is sent implicitly 43 | 44 | # delete unused headers 45 | proxy_set_header Accept ""; 46 | proxy_set_header Accept-Encoding ""; 47 | proxy_set_header Accept-Language ""; 48 | proxy_set_header Accept-Charset ""; 49 | proxy_set_header User-Agent ""; 50 | proxy_set_header Cookie ""; 51 | 52 | # don't send POST-request body 53 | proxy_pass_request_body off; 54 | } 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antage/cdnstats/a342da8658cf7aa9e032733a261b8bca118975a6/bin/.gitkeep -------------------------------------------------------------------------------- /src/cdnstats/analytics.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "string_table" 5 | ) 6 | 7 | type DisplayableStat struct { 8 | requests uint64 9 | Rps uint64 10 | bytes uint64 11 | Bps uint64 12 | } 13 | 14 | type namedValue struct { 15 | Name string 16 | Bytes uint64 17 | } 18 | 19 | type ComposedDisplaybleStats struct { 20 | Title string 21 | 22 | Buckets []string 23 | Servers []string 24 | 25 | Summary DisplayableStat 26 | ByHour [24]DisplayableStat 27 | 28 | ByPath [50]namedValue 29 | ByReferer [50]namedValue 30 | } 31 | 32 | func extractTop(h map[string_table.Id]Stat, table *string_table.StringTable, r []namedValue) { 33 | for i, _ := range r { 34 | for k, s := range h { 35 | if i > 0 && s.Bytes >= r[i-1].Bytes { 36 | continue 37 | } 38 | if r[i].Bytes < s.Bytes { 39 | if name, ok := table.Lookup(k); ok { 40 | r[i] = namedValue{Name: name, Bytes: s.Bytes} 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | func calculateComposedStats(rng *StatRing) *ComposedDisplaybleStats { 48 | data := new(ComposedDisplaybleStats) 49 | for i, s := range rng.ring { 50 | if s != nil { 51 | func() { 52 | s.lock.RLock() 53 | defer s.lock.RUnlock() 54 | 55 | data.ByHour[i].requests = s.Requests 56 | data.ByHour[i].Rps = s.Requests / 3600 57 | data.ByHour[i].bytes = s.Bytes 58 | data.ByHour[i].Bps = s.Bytes / 3600 59 | 60 | data.Summary.requests += s.Requests 61 | data.Summary.bytes += s.Bytes 62 | }() 63 | } 64 | } 65 | data.Summary.Rps = data.Summary.requests / (24 * 3600) 66 | data.Summary.Bps = data.Summary.bytes / (24 * 3600) 67 | 68 | summaryByPath := make(map[string_table.Id]Stat) 69 | summaryByReferer := make(map[string_table.Id]Stat) 70 | 71 | // aggregate all data for last 24 hours 72 | for _, s := range rng.ring { 73 | if s != nil { 74 | func() { 75 | s.lock.RLock() 76 | defer s.lock.RUnlock() 77 | 78 | for _, ps := range s.PathStats { 79 | if sp, ok := summaryByPath[ps.Path]; ok { 80 | summaryByPath[ps.Path] = Stat{sp.Bytes + ps.Bytes} 81 | } else { 82 | summaryByPath[ps.Path] = Stat{ps.Bytes} 83 | } 84 | } 85 | for _, rs := range s.RefererStats { 86 | if sr, ok := summaryByReferer[rs.Referer]; ok { 87 | summaryByReferer[rs.Referer] = Stat{rs.Bytes + sr.Bytes} 88 | } else { 89 | summaryByReferer[rs.Referer] = Stat{rs.Bytes} 90 | } 91 | } 92 | }() 93 | } 94 | } 95 | 96 | // extract top values 97 | extractTop(summaryByPath, pathTable, data.ByPath[:]) 98 | extractTop(summaryByReferer, refererTable, data.ByReferer[:]) 99 | 100 | return data 101 | } 102 | -------------------------------------------------------------------------------- /src/cdnstats/globals.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "string_table" 6 | "sync" 7 | ) 8 | 9 | type ringByString struct { 10 | lock sync.RWMutex 11 | m map[string]*StatRing 12 | } 13 | 14 | var ring = NewStatRing() 15 | var ringByBucket = ringByString{m: make(map[string]*StatRing)} 16 | var ringByServer = ringByString{m: make(map[string]*StatRing)} 17 | 18 | var rx = make(chan *http.Request, 1024) 19 | 20 | var pathTable *string_table.StringTable 21 | var refererTable = string_table.New() 22 | 23 | func (r *ringByString) Lookup(s string) (*StatRing, bool) { 24 | r.lock.RLock() 25 | defer r.lock.RUnlock() 26 | 27 | rng, ok := r.m[s] 28 | return rng, ok 29 | } 30 | 31 | func (r *ringByString) LookupOrCreate(s string) *StatRing { 32 | r.lock.Lock() 33 | defer r.lock.Unlock() 34 | 35 | if rng, ok := r.m[s]; ok { 36 | return rng 37 | } else { 38 | rng := NewStatRing() 39 | r.m[s] = rng 40 | return rng 41 | } 42 | panic("unreachable") 43 | } 44 | 45 | func (r *ringByString) Keys() []string { 46 | r.lock.RLock() 47 | defer r.lock.RUnlock() 48 | 49 | keys := make([]string, len(r.m)) 50 | i := 0 51 | for k, _ := range r.m { 52 | keys[i] = k 53 | i++ 54 | } 55 | return keys 56 | } 57 | -------------------------------------------------------------------------------- /src/cdnstats/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "html/template" 7 | "log" 8 | "net/http" 9 | _ "net/http/pprof" 10 | "os" 11 | "path" 12 | "runtime" 13 | "sort" 14 | "string_table" 15 | ) 16 | 17 | func collect(w http.ResponseWriter, r *http.Request) { 18 | rx <- r 19 | } 20 | 21 | func stats(w http.ResponseWriter, r *http.Request) { 22 | fmt.Fprintf(w, "path names: %d\n", pathTable.Len()) 23 | fmt.Fprintf(w, "referer names: %d\n", refererTable.Len()) 24 | fmt.Fprintf(w, "updater queue length: %d\n", len(rx)) 25 | } 26 | 27 | func renderIndex(w http.ResponseWriter, rng *StatRing, title string) { 28 | t := template.New("base") 29 | funcs := template.FuncMap{"humanizeSize": humanizeSize} 30 | template.Must(t.Funcs(funcs).ParseFiles("templates/index.html.template")) 31 | 32 | data := calculateComposedStats(rng) 33 | 34 | data.Title = title 35 | 36 | data.Buckets = ringByBucket.Keys() 37 | sort.StringSlice(data.Buckets).Sort() 38 | data.Servers = ringByServer.Keys() 39 | sort.StringSlice(data.Servers).Sort() 40 | 41 | err := t.ExecuteTemplate(w, "index.html.template", data) 42 | if err != nil { 43 | log.Printf("TEMPLATE ERROR: %s", err) 44 | } 45 | } 46 | 47 | func index(w http.ResponseWriter, r *http.Request) { 48 | defer func() { 49 | err := recover() 50 | if err != nil { 51 | log.Printf("PANIC: %s", err) 52 | w.Write([]byte("Internal error")) 53 | } 54 | }() 55 | 56 | renderIndex(w, ring, "Global") 57 | } 58 | 59 | func bucketIndex(w http.ResponseWriter, r *http.Request) { 60 | defer func() { 61 | err := recover() 62 | if err != nil { 63 | log.Printf("PANIC: %s", err) 64 | w.Write([]byte("Internal error")) 65 | } 66 | }() 67 | 68 | if bucket, ok := stripPrefix(r.URL.Path, "/bucket/"); ok { 69 | if rng, ok := ringByBucket.Lookup(bucket); ok { 70 | renderIndex(w, rng, fmt.Sprintf("%s bucket", bucket)) 71 | } else { 72 | w.WriteHeader(404) 73 | } 74 | } 75 | } 76 | 77 | func serverIndex(w http.ResponseWriter, r *http.Request) { 78 | defer func() { 79 | err := recover() 80 | if err != nil { 81 | log.Printf("PANIC: %s", err) 82 | w.Write([]byte("Internal error")) 83 | } 84 | }() 85 | 86 | t := template.New("base") 87 | funcs := template.FuncMap{"humanizeSize": humanizeSize} 88 | template.Must(t.Funcs(funcs).ParseFiles("templates/index.html.template")) 89 | 90 | if server, ok := stripPrefix(r.URL.Path, "/server/"); ok { 91 | if rng, ok := ringByServer.Lookup(server); ok { 92 | renderIndex(w, rng, fmt.Sprintf("%s server", server)) 93 | } else { 94 | w.WriteHeader(404) 95 | } 96 | } 97 | } 98 | 99 | var host = flag.String("h", "127.0.0.1", "host address (default 127.0.0.1)") 100 | var port = flag.Int("p", 9090, "port (default 9090)") 101 | var pathHashtableSize = flag.Int("phts", 1000, "path hashtable size (default 1000)") 102 | 103 | func main() { 104 | runtime.GOMAXPROCS(runtime.NumCPU()) 105 | 106 | err := os.Chdir(path.Join(path.Dir(os.Args[0]), "..")) 107 | if err != nil { 108 | log.Fatal(err) 109 | } 110 | 111 | flag.Parse() 112 | 113 | pathTable = string_table.NewPreallocated(*pathHashtableSize) 114 | 115 | http.HandleFunc("/", index) 116 | http.HandleFunc("/collect", collect) 117 | http.HandleFunc("/bucket/", bucketIndex) 118 | http.HandleFunc("/server/", serverIndex) 119 | http.HandleFunc("/stats", stats) 120 | 121 | http.Handle("/favicon.ico", http.NotFoundHandler()) 122 | 123 | go updater(rx) 124 | 125 | err = http.ListenAndServe(fmt.Sprintf("%s:%d", *host, *port), nil) 126 | if err != nil { 127 | log.Fatal(err) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/cdnstats/stats.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sort" 5 | "string_table" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type Stat struct { 11 | Bytes uint64 12 | } 13 | 14 | type StatWithRequests struct { 15 | Stat 16 | Requests uint64 17 | } 18 | 19 | type PathStat struct { 20 | Stat 21 | Path string_table.Id 22 | } 23 | 24 | type RefererStat struct { 25 | Stat 26 | Referer string_table.Id 27 | } 28 | 29 | type PathStatSlice []PathStat 30 | type RefererStatSlice []RefererStat 31 | 32 | type StatByPathAndReferer struct { 33 | StatWithRequests 34 | 35 | PathStats PathStatSlice 36 | RefererStats RefererStatSlice 37 | 38 | // aux fields 39 | lock sync.RWMutex 40 | statByPath map[string_table.Id]Stat 41 | statByReferer map[string_table.Id]Stat 42 | } 43 | 44 | type StatRing struct { 45 | lock sync.Mutex 46 | ring [24]*StatByPathAndReferer 47 | lastHour int 48 | } 49 | 50 | func NewStatByPathAndReferer() *StatByPathAndReferer { 51 | s := new(StatByPathAndReferer) 52 | s.statByPath = make(map[string_table.Id]Stat) 53 | s.statByReferer = make(map[string_table.Id]Stat) 54 | return s 55 | } 56 | 57 | func NewStatRing() (r *StatRing) { 58 | r = new(StatRing) 59 | r.lastHour = -1 60 | return 61 | } 62 | 63 | func (r *StatRing) Current() *StatByPathAndReferer { 64 | r.lock.Lock() 65 | defer r.lock.Unlock() 66 | 67 | currentHour := time.Now().Hour() 68 | if r.lastHour != currentHour { 69 | if r.lastHour >= 0 { 70 | go postProcess(r.ring[r.lastHour]) 71 | } 72 | r.ring[currentHour] = NewStatByPathAndReferer() 73 | r.lastHour = currentHour 74 | } 75 | return r.ring[currentHour] 76 | } 77 | 78 | func postProcess(s *StatByPathAndReferer) { 79 | s.lock.Lock() 80 | defer s.lock.Unlock() 81 | 82 | s.PathStats = make([]PathStat, len(s.statByPath)) 83 | i := 0 84 | for p, cs := range s.statByPath { 85 | s.PathStats[i] = PathStat{Stat{cs.Bytes}, p} 86 | i++ 87 | } 88 | // clear map 89 | s.statByPath = nil 90 | // sort by .Bytes 91 | sort.Sort(s.PathStats) 92 | // truncate slice to 1024 elements 93 | if len(s.PathStats) > 1024 { 94 | newSlice := make([]PathStat, 1024) 95 | copy(newSlice, s.PathStats[len(s.PathStats)-1024:]) 96 | s.PathStats = newSlice 97 | } 98 | 99 | s.RefererStats = make([]RefererStat, len(s.statByReferer)) 100 | i = 0 101 | for r, cs := range s.statByReferer { 102 | s.RefererStats[i] = RefererStat{Stat{cs.Bytes}, r} 103 | i++ 104 | } 105 | // clear map 106 | s.statByReferer = nil 107 | // sort by .Bytes 108 | sort.Sort(s.RefererStats) 109 | // truncate slice to 1024 elements 110 | if len(s.RefererStats) > 1024 { 111 | newSlice := make([]RefererStat, 1024) 112 | copy(newSlice, s.RefererStats[len(s.RefererStats)-1024:]) 113 | s.RefererStats = newSlice 114 | } 115 | 116 | } 117 | 118 | // sort.Interface implementation 119 | func (a PathStatSlice) Len() int { 120 | return len(a) 121 | } 122 | 123 | func (a PathStatSlice) Less(i, j int) bool { 124 | return a[i].Stat.Bytes < a[j].Stat.Bytes 125 | } 126 | 127 | func (a PathStatSlice) Swap(i, j int) { 128 | a[i], a[j] = a[j], a[i] 129 | } 130 | 131 | // sort.Interface implementation 132 | func (a RefererStatSlice) Len() int { 133 | return len(a) 134 | } 135 | 136 | func (a RefererStatSlice) Less(i, j int) bool { 137 | return a[i].Bytes < a[j].Bytes 138 | } 139 | 140 | func (a RefererStatSlice) Swap(i, j int) { 141 | a[i], a[j] = a[j], a[i] 142 | } 143 | -------------------------------------------------------------------------------- /src/cdnstats/updater.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | ) 7 | 8 | func update(r *http.Request, rng *StatRing) { 9 | s := rng.Current() 10 | 11 | s.lock.Lock() 12 | defer s.lock.Unlock() 13 | 14 | s.Requests++ 15 | b, err := strconv.ParseUint(r.Header.Get("X-Bytes-Sent"), 10, 64) 16 | if err == nil { 17 | s.Bytes += b 18 | 19 | referer := normalizeReferer(r.Header.Get("Referer")) 20 | 21 | // copy referer to avoid memory leaks 22 | refererCopy := make([]byte, len(referer)) 23 | copy(refererCopy, referer) 24 | 25 | refererId := refererTable.Store(string(refererCopy)) 26 | if len(referer) > 0 { 27 | if sc, ok := s.statByReferer[refererId]; ok { 28 | s.statByReferer[refererId] = Stat{sc.Bytes + b} 29 | } else { 30 | s.statByReferer[refererId] = Stat{b} 31 | } 32 | } 33 | 34 | path := r.FormValue("uri") 35 | 36 | // copy path to avoid memory leaks 37 | pathCopy := make([]byte, len(path)) 38 | copy(pathCopy, path) 39 | 40 | pathId := pathTable.Store(string(pathCopy)) 41 | if len(path) > 0 { 42 | if sc, ok := s.statByPath[pathId]; ok { 43 | s.statByPath[pathId] = Stat{sc.Bytes + b} 44 | } else { 45 | s.statByPath[pathId] = Stat{b} 46 | } 47 | } 48 | } 49 | } 50 | 51 | func updater(source chan *http.Request) { 52 | for r := range source { 53 | // update global ring 54 | go update(r, ring) 55 | 56 | // update bucket ring 57 | bucket := r.FormValue("bucket") 58 | go update(r, ringByBucket.LookupOrCreate(bucket)) 59 | 60 | // update server ring 61 | server := r.FormValue("s") 62 | go update(r, ringByServer.LookupOrCreate(server)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/cdnstats/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | ) 8 | 9 | func normalizeReferer(r string) string { 10 | if len(r) == 0 { 11 | return r 12 | } else { 13 | result := "" 14 | u, err := url.Parse(r) 15 | if err == nil { 16 | result = u.Host 17 | if strings.HasPrefix(u.Host, "www.") { 18 | result = u.Host[4:] 19 | } 20 | } 21 | return result 22 | } 23 | panic("unreachable") 24 | } 25 | 26 | func humanizeSize(b uint64) string { 27 | if b > 10*1024*1024*1024*1024 { 28 | return fmt.Sprintf("%d TiB", b/(1024*1024*1024*1024)) 29 | } 30 | if b > 10*1024*1024*1024 { 31 | return fmt.Sprintf("%d GiB", b/(1024*1024*1024)) 32 | } 33 | if b > 10*1024*1024 { 34 | return fmt.Sprintf("%d MiB", b/(1024*1024)) 35 | } 36 | if b > 10*1024 { 37 | return fmt.Sprintf("%d KiB", b/1024) 38 | } 39 | return fmt.Sprintf("%d B", b) 40 | } 41 | 42 | func stripPrefix(s, prefix string) (r string, done bool) { 43 | if strings.HasPrefix(s, prefix) { 44 | return s[len(prefix):], true 45 | } 46 | return s, false 47 | } 48 | -------------------------------------------------------------------------------- /src/sequence/sequence.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type Id16 uint16 8 | type Id32 uint32 9 | type Id64 uint64 10 | 11 | type Uint16Sequence struct { 12 | lock sync.RWMutex 13 | lastId Id16 14 | } 15 | 16 | type Uint32Sequence struct { 17 | lock sync.RWMutex 18 | lastId Id32 19 | } 20 | 21 | type Uint64Sequence struct { 22 | lock sync.RWMutex 23 | lastId Id64 24 | } 25 | 26 | func (s *Uint16Sequence) Peek() (id Id16) { 27 | s.lock.RLock() 28 | defer s.lock.RUnlock() 29 | 30 | return s.lastId 31 | } 32 | 33 | func (s *Uint16Sequence) Next() (id Id16) { 34 | s.lock.Lock() 35 | defer s.lock.Unlock() 36 | 37 | id = s.lastId 38 | s.lastId++ 39 | 40 | return id 41 | } 42 | 43 | func (s *Uint32Sequence) Peek() (id Id32) { 44 | s.lock.RLock() 45 | defer s.lock.RUnlock() 46 | 47 | return s.lastId 48 | } 49 | 50 | func (s *Uint32Sequence) Next() (id Id32) { 51 | s.lock.Lock() 52 | defer s.lock.Unlock() 53 | 54 | id = s.lastId 55 | s.lastId++ 56 | 57 | return id 58 | } 59 | 60 | func (s *Uint64Sequence) Peek() (id Id64) { 61 | s.lock.RLock() 62 | defer s.lock.RUnlock() 63 | 64 | return s.lastId 65 | } 66 | 67 | func (s *Uint64Sequence) Next() (id Id64) { 68 | s.lock.Lock() 69 | defer s.lock.Unlock() 70 | 71 | id = s.lastId 72 | s.lastId++ 73 | 74 | return id 75 | } 76 | -------------------------------------------------------------------------------- /src/sequence/sequence_test.go: -------------------------------------------------------------------------------- 1 | package sequence 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestUint16SequenceNew(t *testing.T) { 8 | var s Uint16Sequence 9 | if s.Peek() != 0 { 10 | t.Fail() 11 | } 12 | } 13 | 14 | func TestUint16SequenceNext(t *testing.T) { 15 | var s Uint16Sequence 16 | 17 | currentId := s.Peek() 18 | nextId := s.Next() 19 | 20 | if nextId != currentId { 21 | t.Fail() 22 | } 23 | } 24 | 25 | func TestUint16SequencePeek(t *testing.T) { 26 | var s Uint16Sequence 27 | 28 | prevId := s.Next() 29 | 30 | if (prevId + 1) != s.Peek() { 31 | t.Fail() 32 | } 33 | } 34 | 35 | func TestUint32SequenceNew(t *testing.T) { 36 | var s Uint32Sequence 37 | if s.Peek() != 0 { 38 | t.Fail() 39 | } 40 | } 41 | 42 | func TestUint32SequenceNext(t *testing.T) { 43 | var s Uint32Sequence 44 | 45 | currentId := s.Peek() 46 | nextId := s.Next() 47 | 48 | if nextId != currentId { 49 | t.Fail() 50 | } 51 | } 52 | 53 | func TestUint32SequencePeek(t *testing.T) { 54 | var s Uint32Sequence 55 | 56 | prevId := s.Next() 57 | 58 | if (prevId + 1) != s.Peek() { 59 | t.Fail() 60 | } 61 | } 62 | 63 | func TestUint64SequenceNew(t *testing.T) { 64 | var s Uint64Sequence 65 | if s.Peek() != 0 { 66 | t.Fail() 67 | } 68 | } 69 | 70 | func TestUint64SequenceNext(t *testing.T) { 71 | var s Uint64Sequence 72 | 73 | currentId := s.Peek() 74 | nextId := s.Next() 75 | 76 | if nextId != currentId { 77 | t.Fail() 78 | } 79 | } 80 | 81 | func TestUint64SequencePeek(t *testing.T) { 82 | var s Uint64Sequence 83 | 84 | prevId := s.Next() 85 | 86 | if (prevId + 1) != s.Peek() { 87 | t.Fail() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/string_table/string_table.go: -------------------------------------------------------------------------------- 1 | package string_table 2 | 3 | import ( 4 | "sequence" 5 | "sync" 6 | ) 7 | 8 | type Id uint32 9 | 10 | type StringTable struct { 11 | lock sync.RWMutex 12 | sequence sequence.Uint32Sequence 13 | stringById map[Id]string 14 | idByString map[string]Id 15 | } 16 | 17 | func New() (t *StringTable) { 18 | t = new(StringTable) 19 | t.stringById = make(map[Id]string) 20 | t.idByString = make(map[string]Id) 21 | return t 22 | } 23 | 24 | func NewPreallocated(size int) (t *StringTable) { 25 | t = new(StringTable) 26 | t.stringById = make(map[Id]string, size) 27 | t.idByString = make(map[string]Id, size) 28 | return t 29 | } 30 | 31 | func (t *StringTable) Store(s string) (id Id) { 32 | t.lock.Lock() 33 | defer t.lock.Unlock() 34 | 35 | id, ok := t.idByString[s] 36 | if ok { 37 | return 38 | } else { 39 | id := Id(t.sequence.Next()) 40 | t.stringById[id] = s 41 | t.idByString[s] = id 42 | return id 43 | } 44 | panic("unreachable") 45 | } 46 | 47 | func (t *StringTable) Lookup(id Id) (s string, ok bool) { 48 | t.lock.RLock() 49 | defer t.lock.RUnlock() 50 | 51 | s, ok = t.stringById[id] 52 | return 53 | } 54 | 55 | func (t *StringTable) Len() uint32 { 56 | t.lock.RLock() 57 | defer t.lock.RUnlock() 58 | 59 | return uint32(t.sequence.Peek()) 60 | } 61 | -------------------------------------------------------------------------------- /src/string_table/string_table_test.go: -------------------------------------------------------------------------------- 1 | package string_table 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestStringTableNew(t *testing.T) { 8 | st := New() 9 | 10 | if st.Len() != 0 { 11 | t.Fail() 12 | } 13 | } 14 | 15 | func TestStringTableStore(t *testing.T) { 16 | st := New() 17 | id := st.Store("abcdefgh") 18 | id2 := st.Store("abcdefgh") 19 | 20 | if id != id2 { 21 | t.Fail() 22 | } 23 | 24 | if st.Len() != 1 { 25 | t.Fail() 26 | } 27 | } 28 | 29 | func TestStringTableLookup(t *testing.T) { 30 | st := New() 31 | 32 | s := "abcdefgh" 33 | id := st.Store(s) 34 | 35 | s2, ok := st.Lookup(id) 36 | if !ok { 37 | t.Fail() 38 | } 39 | if s2 != s { 40 | t.Fail() 41 | } 42 | } 43 | 44 | func TestStringTableLookupNotExistedString(t *testing.T) { 45 | st := New() 46 | 47 | if _, ok := st.Lookup(9999); ok { 48 | t.Fail() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /templates/index.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CDN Stats 5 | 6 | 21 | 22 | 23 | 52 |
53 |
54 |
55 |

CDN Stats

56 | 57 |

{{.Title}}

58 | 59 |

Summary

60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
Requests/sec (average):{{.Summary.Rps}}
Bytes/sec (average):{{.Summary.Bps | humanizeSize}}
71 | 72 |

By hours

73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | {{range $index, $el := .ByHour}} 83 | 84 | 85 | 86 | 87 | 88 | {{end}} 89 | 90 |
HourReqs/secBytes/sec
{{$index}}{{$el.Rps}}{{$el.Bps | humanizeSize}}
91 | 92 |

By path

93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | {{range .ByPath}} 102 | 103 | 104 | 105 | 106 | {{end}} 107 | 108 |
PathBytes
{{.Name}}{{.Bytes | humanizeSize}}
109 | 110 |

By referer

111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | {{range .ByReferer}} 120 | 121 | 122 | 123 | 124 | {{end}} 125 | 126 |
RefererBytes
{{.Name}}{{.Bytes | humanizeSize}}
127 |
128 |
129 |
130 | 131 | 132 | 133 | 134 | --------------------------------------------------------------------------------