main.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. package main
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "flag"
  6. "fmt"
  7. "html/template"
  8. "io"
  9. "io/ioutil"
  10. "log"
  11. "net/http"
  12. "os"
  13. "path"
  14. "path/filepath"
  15. "regexp"
  16. "sort"
  17. "strings"
  18. "sync"
  19. "time"
  20. )
  21. var (
  22. keyFile = flag.String("key", "", "Key file")
  23. certFile = flag.String("cert", "", "Certificate file")
  24. dbDir = flag.String("db", "", "Database directory")
  25. port = flag.Int("port", 8443, "Listen port")
  26. tpl *template.Template
  27. )
  28. type category struct {
  29. Key string
  30. Descr string
  31. Unit string
  32. }
  33. var categories = []category{
  34. {Key: "totFiles", Descr: "Files Managed per Node", Unit: ""},
  35. {Key: "maxFiles", Descr: "Files in Largest Repo", Unit: ""},
  36. {Key: "totMiB", Descr: "Data Managed per Node", Unit: "MiB"},
  37. {Key: "maxMiB", Descr: "Data in Largest Repo", Unit: "MiB"},
  38. {Key: "numNodes", Descr: "Number of Nodes in Cluster", Unit: ""},
  39. {Key: "numRepos", Descr: "Number of Repositories Configured", Unit: ""},
  40. {Key: "memoryUsage", Descr: "Memory Usage", Unit: "MiB"},
  41. {Key: "memorySize", Descr: "System Memory", Unit: "MiB"},
  42. {Key: "sha256Perf", Descr: "SHA-256 Hashing Performance", Unit: "MiB/s"},
  43. }
  44. var numRe = regexp.MustCompile(`\d\d\d$`)
  45. var funcs = map[string]interface{}{
  46. "number": func(n interface{}) string {
  47. var s string
  48. switch n := n.(type) {
  49. case int:
  50. s = fmt.Sprint(n)
  51. case float64:
  52. s = fmt.Sprintf("%.02f", n)
  53. default:
  54. return fmt.Sprint(n)
  55. }
  56. var b bytes.Buffer
  57. l := len(s)
  58. for i := range s {
  59. b.Write([]byte{s[i]})
  60. if (l-i)%3 == 1 {
  61. b.WriteString(",")
  62. }
  63. }
  64. return b.String()
  65. },
  66. "commatize": commatize,
  67. }
  68. func main() {
  69. flag.Parse()
  70. log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
  71. fd, err := os.Open("static/index.html")
  72. if err != nil {
  73. log.Fatal(err)
  74. }
  75. bs, err := ioutil.ReadAll(fd)
  76. if err != nil {
  77. log.Fatal(err)
  78. }
  79. fd.Close()
  80. tpl = template.Must(template.New("index.html").Funcs(funcs).Parse(string(bs)))
  81. http.HandleFunc("/", rootHandler)
  82. http.HandleFunc("/newdata", newDataHandler)
  83. http.HandleFunc("/report", reportHandler)
  84. http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
  85. err = http.ListenAndServeTLS(fmt.Sprintf(":%d", *port), *certFile, *keyFile, nil)
  86. if err != nil {
  87. log.Fatal(err)
  88. }
  89. }
  90. func rootHandler(w http.ResponseWriter, r *http.Request) {
  91. if r.URL.Path == "/" || r.URL.Path == "/index.html" {
  92. k := timestamp()
  93. rep := getReport(k)
  94. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  95. err := tpl.Execute(w, rep)
  96. if err != nil {
  97. log.Println(err)
  98. }
  99. } else {
  100. http.Error(w, "Not found", 404)
  101. }
  102. }
  103. func reportHandler(w http.ResponseWriter, r *http.Request) {
  104. w.Header().Set("Content-Type", "application/json")
  105. k := timestamp()
  106. rep := getReport(k)
  107. json.NewEncoder(w).Encode(rep)
  108. }
  109. func newDataHandler(w http.ResponseWriter, r *http.Request) {
  110. today := time.Now().Format("20060102")
  111. dir := filepath.Join(*dbDir, today)
  112. ensureDir(dir, 0700)
  113. var m map[string]interface{}
  114. lr := &io.LimitedReader{R: r.Body, N: 10240}
  115. err := json.NewDecoder(lr).Decode(&m)
  116. if err != nil {
  117. log.Println(err)
  118. http.Error(w, err.Error(), 500)
  119. return
  120. }
  121. id, ok := m["uniqueID"]
  122. if ok {
  123. idStr, ok := id.(string)
  124. if !ok {
  125. if err != nil {
  126. log.Println("No ID")
  127. http.Error(w, "No ID", 500)
  128. return
  129. }
  130. }
  131. f, err := os.Create(path.Join(dir, idStr+".json"))
  132. if err != nil {
  133. log.Println(err)
  134. http.Error(w, err.Error(), 500)
  135. return
  136. }
  137. json.NewEncoder(f).Encode(m)
  138. f.Close()
  139. } else {
  140. log.Println("No ID")
  141. http.Error(w, "No ID", 500)
  142. return
  143. }
  144. }
  145. type report struct {
  146. UniqueID string
  147. Version string
  148. Platform string
  149. NumRepos int
  150. NumNodes int
  151. TotFiles int
  152. RepoMaxFiles int
  153. TotMiB int
  154. RepoMaxMiB int
  155. MemoryUsageMiB int
  156. SHA256Perf float64
  157. MemorySize int
  158. }
  159. func fileList() ([]string, error) {
  160. files := make(map[string]string)
  161. t0 := time.Now().Add(-24 * time.Hour).Format("20060102")
  162. t1 := time.Now().Format("20060102")
  163. dir := filepath.Join(*dbDir, t0)
  164. gr, err := filepath.Glob(filepath.Join(dir, "*.json"))
  165. if err != nil {
  166. return nil, err
  167. }
  168. for _, f := range gr {
  169. bn := filepath.Base(f)
  170. files[bn] = f
  171. }
  172. dir = filepath.Join(*dbDir, t1)
  173. gr, err = filepath.Glob(filepath.Join(dir, "*.json"))
  174. if err != nil {
  175. return nil, err
  176. }
  177. for _, f := range gr {
  178. bn := filepath.Base(f)
  179. files[bn] = f
  180. }
  181. l := make([]string, 0, len(files))
  182. for _, f := range files {
  183. l = append(l, f)
  184. }
  185. return l, nil
  186. }
  187. var reportCache map[string]interface{}
  188. var reportMutex sync.Mutex
  189. func getReport(key string) map[string]interface{} {
  190. reportMutex.Lock()
  191. defer reportMutex.Unlock()
  192. if k := reportCache["key"]; k == key {
  193. return reportCache
  194. }
  195. files, err := fileList()
  196. if err != nil {
  197. return nil
  198. }
  199. nodes := 0
  200. var versions []string
  201. var platforms []string
  202. var oses []string
  203. var numRepos []int
  204. var numNodes []int
  205. var totFiles []int
  206. var maxFiles []int
  207. var totMiB []int
  208. var maxMiB []int
  209. var memoryUsage []int
  210. var sha256Perf []float64
  211. var memorySize []int
  212. for _, fn := range files {
  213. f, err := os.Open(fn)
  214. if err != nil {
  215. continue
  216. }
  217. var rep report
  218. err = json.NewDecoder(f).Decode(&rep)
  219. if err != nil {
  220. continue
  221. }
  222. f.Close()
  223. nodes++
  224. versions = append(versions, transformVersion(rep.Version))
  225. platforms = append(platforms, rep.Platform)
  226. ps := strings.Split(rep.Platform, "-")
  227. oses = append(oses, ps[0])
  228. if rep.NumRepos > 0 {
  229. numRepos = append(numRepos, rep.NumRepos)
  230. }
  231. if rep.NumNodes > 0 {
  232. numNodes = append(numNodes, rep.NumNodes)
  233. }
  234. if rep.TotFiles > 0 {
  235. totFiles = append(totFiles, rep.TotFiles)
  236. }
  237. if rep.RepoMaxFiles > 0 {
  238. maxFiles = append(maxFiles, rep.RepoMaxFiles)
  239. }
  240. if rep.TotMiB > 0 {
  241. totMiB = append(totMiB, rep.TotMiB)
  242. }
  243. if rep.RepoMaxMiB > 0 {
  244. maxMiB = append(maxMiB, rep.RepoMaxMiB)
  245. }
  246. if rep.MemoryUsageMiB > 0 {
  247. memoryUsage = append(memoryUsage, rep.MemoryUsageMiB)
  248. }
  249. if rep.SHA256Perf > 0 {
  250. sha256Perf = append(sha256Perf, rep.SHA256Perf)
  251. }
  252. if rep.MemorySize > 0 {
  253. memorySize = append(memorySize, rep.MemorySize)
  254. }
  255. }
  256. r := make(map[string]interface{})
  257. r["key"] = key
  258. r["nodes"] = nodes
  259. r["categories"] = categories
  260. r["versions"] = analyticsFor(versions)
  261. r["platforms"] = analyticsFor(platforms)
  262. r["os"] = analyticsFor(oses)
  263. r["numRepos"] = statsForInts(numRepos)
  264. r["numNodes"] = statsForInts(numNodes)
  265. r["totFiles"] = statsForInts(totFiles)
  266. r["maxFiles"] = statsForInts(maxFiles)
  267. r["totMiB"] = statsForInts(totMiB)
  268. r["maxMiB"] = statsForInts(maxMiB)
  269. r["memoryUsage"] = statsForInts(memoryUsage)
  270. r["sha256Perf"] = statsForFloats(sha256Perf)
  271. r["memorySize"] = statsForInts(memorySize)
  272. reportCache = r
  273. return r
  274. }
  275. type analytic struct {
  276. Key string
  277. Count int
  278. Percentage float64
  279. }
  280. type analyticList []analytic
  281. func (l analyticList) Less(a, b int) bool {
  282. return l[b].Count < l[a].Count // inverse
  283. }
  284. func (l analyticList) Swap(a, b int) {
  285. l[a], l[b] = l[b], l[a]
  286. }
  287. func (l analyticList) Len() int {
  288. return len(l)
  289. }
  290. // Returns a list of frequency analytics for a given list of strings.
  291. func analyticsFor(ss []string) []analytic {
  292. m := make(map[string]int)
  293. t := 0
  294. for _, s := range ss {
  295. m[s]++
  296. t++
  297. }
  298. l := make([]analytic, 0, len(m))
  299. for k, c := range m {
  300. l = append(l, analytic{k, c, 100 * float64(c) / float64(t)})
  301. }
  302. sort.Sort(analyticList(l))
  303. return l
  304. }
  305. func statsForInts(data []int) map[string]int {
  306. sort.Ints(data)
  307. res := make(map[string]int, 4)
  308. if len(data) == 0 {
  309. return res
  310. }
  311. res["fp"] = data[int(float64(len(data))*0.05)]
  312. res["med"] = data[len(data)/2]
  313. res["nfp"] = data[int(float64(len(data))*0.95)]
  314. res["max"] = data[len(data)-1]
  315. return res
  316. }
  317. func statsForFloats(data []float64) map[string]float64 {
  318. sort.Float64s(data)
  319. res := make(map[string]float64, 4)
  320. if len(data) == 0 {
  321. return res
  322. }
  323. res["fp"] = data[int(float64(len(data))*0.05)]
  324. res["med"] = data[len(data)/2]
  325. res["nfp"] = data[int(float64(len(data))*0.95)]
  326. res["max"] = data[len(data)-1]
  327. return res
  328. }
  329. func ensureDir(dir string, mode int) {
  330. fi, err := os.Stat(dir)
  331. if os.IsNotExist(err) {
  332. os.MkdirAll(dir, 0700)
  333. } else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
  334. os.Chmod(dir, os.FileMode(mode))
  335. }
  336. }
  337. var vRe = regexp.MustCompile(`^(v\d+\.\d+\.\d+)-.+`)
  338. // transformVersion returns a version number formatted correctly, with all
  339. // development versions aggregated into one.
  340. func transformVersion(v string) string {
  341. if !strings.HasPrefix(v, "v") {
  342. v = "v" + v
  343. }
  344. if m := vRe.FindStringSubmatch(v); len(m) > 0 {
  345. return m[1] + " (+dev)"
  346. }
  347. return v
  348. }
  349. // commatize returns a number with sep as thousands separators. Handles
  350. // integers and plain floats.
  351. func commatize(sep, s string) string {
  352. var b bytes.Buffer
  353. fs := strings.SplitN(s, ".", 2)
  354. l := len(fs[0])
  355. for i := range fs[0] {
  356. b.Write([]byte{s[i]})
  357. if i < l-1 && (l-i)%3 == 1 {
  358. b.WriteString(sep)
  359. }
  360. }
  361. if len(fs) > 1 && len(fs[1]) > 0 {
  362. b.WriteString(".")
  363. b.WriteString(fs[1])
  364. }
  365. return b.String()
  366. }
  367. // timestamp returns a time stamp for the current hour, to be used as a cache key
  368. func timestamp() string {
  369. return time.Now().Format("20060102T15")
  370. }