├── .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 | Requests/sec (average): |
64 | {{.Summary.Rps}} |
65 |
66 |
67 | Bytes/sec (average): |
68 | {{.Summary.Bps | humanizeSize}} |
69 |
70 |
71 |
72 |
By hours
73 |
74 |
75 |
76 | Hour |
77 | Reqs/sec |
78 | Bytes/sec |
79 |
80 |
81 |
82 | {{range $index, $el := .ByHour}}
83 |
84 | {{$index}} |
85 | {{$el.Rps}} |
86 | {{$el.Bps | humanizeSize}} |
87 |
88 | {{end}}
89 |
90 |
91 |
92 |
By path
93 |
94 |
95 |
96 | Path |
97 | Bytes |
98 |
99 |
100 |
101 | {{range .ByPath}}
102 |
103 | {{.Name}} |
104 | {{.Bytes | humanizeSize}} |
105 |
106 | {{end}}
107 |
108 |
109 |
110 |
By referer
111 |
112 |
113 |
114 | Referer |
115 | Bytes |
116 |
117 |
118 |
119 | {{range .ByReferer}}
120 |
121 | {{.Name}} |
122 | {{.Bytes | humanizeSize}} |
123 |
124 | {{end}}
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
--------------------------------------------------------------------------------