main.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. package main
  2. import (
  3. "bytes"
  4. "crypto/tls"
  5. "database/sql"
  6. "encoding/json"
  7. "fmt"
  8. "html/template"
  9. "io"
  10. "io/ioutil"
  11. "log"
  12. "net/http"
  13. "os"
  14. "regexp"
  15. "sort"
  16. "strings"
  17. "sync"
  18. "time"
  19. _ "github.com/lib/pq"
  20. )
  21. var (
  22. keyFile = getEnvDefault("UR_KEY_FILE", "key.pem")
  23. certFile = getEnvDefault("UR_CRT_FILE", "crt.pem")
  24. dbConn = getEnvDefault("UR_DB_URL", "postgres://user:password@localhost/ur?sslmode=disable")
  25. listenAddr = getEnvDefault("UR_LISTEN", "0.0.0.0:8443")
  26. tpl *template.Template
  27. compilerRe = regexp.MustCompile(`\(([A-Za-z0-9()., -]+) [\w-]+ \w+\) ([\w@-]+)`)
  28. aggregateVersions = []string{"v0.7", "v0.8", "v0.9", "v0.10"}
  29. )
  30. var funcs = map[string]interface{}{
  31. "commatize": commatize,
  32. "number": number,
  33. }
  34. func getEnvDefault(key, def string) string {
  35. if val := os.Getenv(key); val != "" {
  36. return val
  37. }
  38. return def
  39. }
  40. type report struct {
  41. Received time.Time // Only from DB
  42. UniqueID string
  43. Version string
  44. LongVersion string
  45. Platform string
  46. NumFolders int
  47. NumDevices int
  48. TotFiles int
  49. FolderMaxFiles int
  50. TotMiB int
  51. FolderMaxMiB int
  52. MemoryUsageMiB int
  53. SHA256Perf float64
  54. MemorySize int
  55. Date string
  56. }
  57. func (r *report) Validate() error {
  58. if r.UniqueID == "" || r.Version == "" || r.Platform == "" {
  59. return fmt.Errorf("missing required field")
  60. }
  61. if len(r.Date) != 8 {
  62. return fmt.Errorf("date not initialized")
  63. }
  64. return nil
  65. }
  66. func setupDB(db *sql.DB) error {
  67. _, err := db.Exec(`CREATE TABLE IF NOT EXISTS Reports (
  68. Received TIMESTAMP NOT NULL,
  69. UniqueID VARCHAR(32) NOT NULL,
  70. Version VARCHAR(32) NOT NULL,
  71. LongVersion VARCHAR(256) NOT NULL,
  72. Platform VARCHAR(32) NOT NULL,
  73. NumFolders INTEGER NOT NULL,
  74. NumDevices INTEGER NOT NULL,
  75. TotFiles INTEGER NOT NULL,
  76. FolderMaxFiles INTEGER NOT NULL,
  77. TotMiB INTEGER NOT NULL,
  78. FolderMaxMiB INTEGER NOT NULL,
  79. MemoryUsageMiB INTEGER NOT NULL,
  80. SHA256Perf DOUBLE PRECISION NOT NULL,
  81. MemorySize INTEGER NOT NULL,
  82. Date VARCHAR(8) NOT NULL
  83. )`)
  84. if err != nil {
  85. return err
  86. }
  87. row := db.QueryRow(`SELECT 'UniqueIDIndex'::regclass`)
  88. if err := row.Scan(nil); err != nil {
  89. _, err = db.Exec(`CREATE UNIQUE INDEX UniqueIDIndex ON Reports (Date, UniqueID)`)
  90. }
  91. row = db.QueryRow(`SELECT 'ReceivedIndex'::regclass`)
  92. if err := row.Scan(nil); err != nil {
  93. _, err = db.Exec(`CREATE INDEX ReceivedIndex ON Reports (Received)`)
  94. }
  95. return err
  96. }
  97. func insertReport(db *sql.DB, r report) error {
  98. _, err := db.Exec(`INSERT INTO Reports VALUES (TIMEZONE('UTC', NOW()), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`,
  99. r.UniqueID, r.Version, r.LongVersion, r.Platform, r.NumFolders,
  100. r.NumDevices, r.TotFiles, r.FolderMaxFiles, r.TotMiB, r.FolderMaxMiB,
  101. r.MemoryUsageMiB, r.SHA256Perf, r.MemorySize, r.Date)
  102. return err
  103. }
  104. type withDBFunc func(*sql.DB, http.ResponseWriter, *http.Request)
  105. func withDB(db *sql.DB, f withDBFunc) http.HandlerFunc {
  106. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  107. f(db, w, r)
  108. })
  109. }
  110. func main() {
  111. log.SetFlags(log.Ltime | log.Ldate)
  112. log.SetOutput(os.Stdout)
  113. // Template
  114. fd, err := os.Open("static/index.html")
  115. if err != nil {
  116. log.Fatalln("template:", err)
  117. }
  118. bs, err := ioutil.ReadAll(fd)
  119. if err != nil {
  120. log.Fatalln("template:", err)
  121. }
  122. fd.Close()
  123. tpl = template.Must(template.New("index.html").Funcs(funcs).Parse(string(bs)))
  124. // DB
  125. db, err := sql.Open("postgres", dbConn)
  126. if err != nil {
  127. log.Fatalln("database:", err)
  128. }
  129. err = setupDB(db)
  130. if err != nil {
  131. log.Fatalln("database:", err)
  132. }
  133. // TLS
  134. cert, err := tls.LoadX509KeyPair(certFile, keyFile)
  135. if err != nil {
  136. log.Fatalln("tls:", err)
  137. }
  138. cfg := &tls.Config{
  139. Certificates: []tls.Certificate{cert},
  140. SessionTicketsDisabled: true,
  141. }
  142. // HTTPS
  143. listener, err := tls.Listen("tcp", listenAddr, cfg)
  144. if err != nil {
  145. log.Fatalln("https:", err)
  146. }
  147. srv := http.Server{
  148. ReadTimeout: 5 * time.Second,
  149. WriteTimeout: 5 * time.Second,
  150. }
  151. http.HandleFunc("/", withDB(db, rootHandler))
  152. http.HandleFunc("/newdata", withDB(db, newDataHandler))
  153. http.HandleFunc("/summary.json", withDB(db, summaryHandler))
  154. http.HandleFunc("/movement.json", withDB(db, movementHandler))
  155. http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
  156. err = srv.Serve(listener)
  157. if err != nil {
  158. log.Fatalln("https:", err)
  159. }
  160. }
  161. var (
  162. cacheData []byte
  163. cacheTime time.Time
  164. cacheMut sync.Mutex
  165. )
  166. const maxCacheTime = 5 * 60 * time.Second
  167. func rootHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  168. if r.URL.Path == "/" || r.URL.Path == "/index.html" {
  169. cacheMut.Lock()
  170. defer cacheMut.Unlock()
  171. if time.Since(cacheTime) > maxCacheTime {
  172. rep := getReport(db)
  173. buf := new(bytes.Buffer)
  174. err := tpl.Execute(buf, rep)
  175. if err != nil {
  176. log.Println(err)
  177. http.Error(w, "Template Error", http.StatusInternalServerError)
  178. return
  179. }
  180. cacheData = buf.Bytes()
  181. cacheTime = time.Now()
  182. }
  183. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  184. w.Write(cacheData)
  185. } else {
  186. http.Error(w, "Not found", 404)
  187. return
  188. }
  189. }
  190. func newDataHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  191. defer r.Body.Close()
  192. var rep report
  193. rep.Date = time.Now().UTC().Format("20060102")
  194. lr := &io.LimitedReader{R: r.Body, N: 10240}
  195. if err := json.NewDecoder(lr).Decode(&rep); err != nil {
  196. log.Println("json decode:", err)
  197. http.Error(w, "JSON Decode Error", http.StatusInternalServerError)
  198. return
  199. }
  200. if err := rep.Validate(); err != nil {
  201. log.Println("validate:", err)
  202. log.Printf("%#v", rep)
  203. http.Error(w, "Validation Error", http.StatusInternalServerError)
  204. return
  205. }
  206. if err := insertReport(db, rep); err != nil {
  207. log.Println("insert:", err)
  208. log.Printf("%#v", rep)
  209. http.Error(w, "Database Error", http.StatusInternalServerError)
  210. return
  211. }
  212. }
  213. func summaryHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  214. s, err := getSummary(db)
  215. if err != nil {
  216. log.Println("summaryHandler:", err)
  217. http.Error(w, "Database Error", http.StatusInternalServerError)
  218. return
  219. }
  220. bs, err := s.MarshalJSON()
  221. if err != nil {
  222. log.Println("summaryHandler:", err)
  223. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  224. return
  225. }
  226. w.Header().Set("Content-Type", "application/json")
  227. w.Write(bs)
  228. }
  229. func movementHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  230. s, err := getMovement(db)
  231. if err != nil {
  232. log.Println("movementHandler:", err)
  233. http.Error(w, "Database Error", http.StatusInternalServerError)
  234. return
  235. }
  236. bs, err := json.Marshal(s)
  237. if err != nil {
  238. log.Println("movementHandler:", err)
  239. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  240. return
  241. }
  242. w.Header().Set("Content-Type", "application/json")
  243. w.Write(bs)
  244. }
  245. type category struct {
  246. Values [4]float64
  247. Key string
  248. Descr string
  249. Unit string
  250. Binary bool
  251. }
  252. func getReport(db *sql.DB) map[string]interface{} {
  253. nodes := 0
  254. var versions []string
  255. var platforms []string
  256. var oses []string
  257. var numFolders []int
  258. var numDevices []int
  259. var totFiles []int
  260. var maxFiles []int
  261. var totMiB []int
  262. var maxMiB []int
  263. var memoryUsage []int
  264. var sha256Perf []float64
  265. var memorySize []int
  266. var compilers []string
  267. var builders []string
  268. rows, err := db.Query(`SELECT * FROM Reports WHERE Received > now() - '1 day'::INTERVAL`)
  269. if err != nil {
  270. log.Println("sql:", err)
  271. return nil
  272. }
  273. defer rows.Close()
  274. for rows.Next() {
  275. var rep report
  276. err := rows.Scan(&rep.Received, &rep.UniqueID, &rep.Version,
  277. &rep.LongVersion, &rep.Platform, &rep.NumFolders, &rep.NumDevices,
  278. &rep.TotFiles, &rep.FolderMaxFiles, &rep.TotMiB, &rep.FolderMaxMiB,
  279. &rep.MemoryUsageMiB, &rep.SHA256Perf, &rep.MemorySize, &rep.Date)
  280. if err != nil {
  281. log.Println("sql:", err)
  282. return nil
  283. }
  284. nodes++
  285. versions = append(versions, transformVersion(rep.Version))
  286. platforms = append(platforms, rep.Platform)
  287. ps := strings.Split(rep.Platform, "-")
  288. oses = append(oses, ps[0])
  289. if m := compilerRe.FindStringSubmatch(rep.LongVersion); len(m) == 3 {
  290. compilers = append(compilers, m[1])
  291. builders = append(builders, m[2])
  292. }
  293. if rep.NumFolders > 0 {
  294. numFolders = append(numFolders, rep.NumFolders)
  295. }
  296. if rep.NumDevices > 0 {
  297. numDevices = append(numDevices, rep.NumDevices)
  298. }
  299. if rep.TotFiles > 0 {
  300. totFiles = append(totFiles, rep.TotFiles)
  301. }
  302. if rep.FolderMaxFiles > 0 {
  303. maxFiles = append(maxFiles, rep.FolderMaxFiles)
  304. }
  305. if rep.TotMiB > 0 {
  306. totMiB = append(totMiB, rep.TotMiB*(1<<20))
  307. }
  308. if rep.FolderMaxMiB > 0 {
  309. maxMiB = append(maxMiB, rep.FolderMaxMiB*(1<<20))
  310. }
  311. if rep.MemoryUsageMiB > 0 {
  312. memoryUsage = append(memoryUsage, rep.MemoryUsageMiB*(1<<20))
  313. }
  314. if rep.SHA256Perf > 0 {
  315. sha256Perf = append(sha256Perf, rep.SHA256Perf*(1<<20))
  316. }
  317. if rep.MemorySize > 0 {
  318. memorySize = append(memorySize, rep.MemorySize*(1<<20))
  319. }
  320. }
  321. var categories []category
  322. categories = append(categories, category{
  323. Values: statsForInts(totFiles),
  324. Descr: "Files Managed per Device",
  325. })
  326. categories = append(categories, category{
  327. Values: statsForInts(maxFiles),
  328. Descr: "Files in Largest Folder",
  329. })
  330. categories = append(categories, category{
  331. Values: statsForInts(totMiB),
  332. Descr: "Data Managed per Device",
  333. Unit: "B",
  334. Binary: true,
  335. })
  336. categories = append(categories, category{
  337. Values: statsForInts(maxMiB),
  338. Descr: "Data in Largest Folder",
  339. Unit: "B",
  340. Binary: true,
  341. })
  342. categories = append(categories, category{
  343. Values: statsForInts(numDevices),
  344. Descr: "Number of Devices in Cluster",
  345. })
  346. categories = append(categories, category{
  347. Values: statsForInts(numFolders),
  348. Descr: "Number of Folders Configured",
  349. })
  350. categories = append(categories, category{
  351. Values: statsForInts(memoryUsage),
  352. Descr: "Memory Usage",
  353. Unit: "B",
  354. Binary: true,
  355. })
  356. categories = append(categories, category{
  357. Values: statsForInts(memorySize),
  358. Descr: "System Memory",
  359. Unit: "B",
  360. Binary: true,
  361. })
  362. categories = append(categories, category{
  363. Values: statsForFloats(sha256Perf),
  364. Descr: "SHA-256 Hashing Performance",
  365. Unit: "B/s",
  366. Binary: true,
  367. })
  368. r := make(map[string]interface{})
  369. r["nodes"] = nodes
  370. r["categories"] = categories
  371. r["versions"] = analyticsFor(versions, 10)
  372. r["platforms"] = analyticsFor(platforms, 0)
  373. r["os"] = analyticsFor(oses, 0)
  374. r["compilers"] = analyticsFor(compilers, 12)
  375. r["builders"] = analyticsFor(builders, 12)
  376. return r
  377. }
  378. func ensureDir(dir string, mode int) {
  379. fi, err := os.Stat(dir)
  380. if os.IsNotExist(err) {
  381. os.MkdirAll(dir, 0700)
  382. } else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
  383. os.Chmod(dir, os.FileMode(mode))
  384. }
  385. }
  386. var vRe = regexp.MustCompile(`^(v\d+\.\d+\.\d+(?:-[a-z]\w+)?)[+\.-]`)
  387. // transformVersion returns a version number formatted correctly, with all
  388. // development versions aggregated into one.
  389. func transformVersion(v string) string {
  390. if v == "unknown-dev" {
  391. return v
  392. }
  393. if !strings.HasPrefix(v, "v") {
  394. v = "v" + v
  395. }
  396. if m := vRe.FindStringSubmatch(v); len(m) > 0 {
  397. return m[1] + " (+dev)"
  398. }
  399. // Truncate old versions to just the generation part
  400. for _, agg := range aggregateVersions {
  401. if strings.HasPrefix(v, agg) {
  402. return agg + ".x"
  403. }
  404. }
  405. return v
  406. }
  407. type summary struct {
  408. versions map[string]int // version string to count index
  409. rows map[string][]int // date to list of counts
  410. }
  411. func newSummary() summary {
  412. return summary{
  413. versions: make(map[string]int),
  414. rows: make(map[string][]int),
  415. }
  416. }
  417. func (s *summary) setCount(date, version string, count int) {
  418. idx, ok := s.versions[version]
  419. if !ok {
  420. idx = len(s.versions)
  421. s.versions[version] = idx
  422. }
  423. row := s.rows[date]
  424. if len(row) <= idx {
  425. old := row
  426. row = make([]int, idx+1)
  427. copy(row, old)
  428. s.rows[date] = row
  429. }
  430. row[idx] = count
  431. }
  432. func (s *summary) MarshalJSON() ([]byte, error) {
  433. var versions []string
  434. for v := range s.versions {
  435. versions = append(versions, v)
  436. }
  437. sort.Strings(versions)
  438. headerRow := []interface{}{"Day"}
  439. for _, v := range versions {
  440. headerRow = append(headerRow, v)
  441. }
  442. var table [][]interface{}
  443. table = append(table, headerRow)
  444. var dates []string
  445. for k := range s.rows {
  446. dates = append(dates, k)
  447. }
  448. sort.Strings(dates)
  449. for _, date := range dates {
  450. row := []interface{}{date}
  451. for _, ver := range versions {
  452. idx := s.versions[ver]
  453. if len(s.rows[date]) > idx && s.rows[date][idx] > 0 {
  454. row = append(row, s.rows[date][idx])
  455. } else {
  456. row = append(row, nil)
  457. }
  458. }
  459. table = append(table, row)
  460. }
  461. return json.Marshal(table)
  462. }
  463. func getSummary(db *sql.DB) (summary, error) {
  464. s := newSummary()
  465. rows, err := db.Query(`SELECT Day, Version, Count FROM VersionSummary WHERE Day > now() - '1 year'::INTERVAL;`)
  466. if err != nil {
  467. return summary{}, err
  468. }
  469. defer rows.Close()
  470. for rows.Next() {
  471. var day time.Time
  472. var ver string
  473. var num int
  474. err := rows.Scan(&day, &ver, &num)
  475. if err != nil {
  476. return summary{}, err
  477. }
  478. if ver == "v0.0" {
  479. // ?
  480. continue
  481. }
  482. // SUPER UGLY HACK to avoid having to do sorting properly
  483. if len(ver) == 4 { // v0.x
  484. ver = ver[:3] + "0" + ver[3:] // now v0.0x
  485. }
  486. s.setCount(day.Format("2006-01-02"), ver, num)
  487. }
  488. return s, nil
  489. }
  490. func getMovement(db *sql.DB) ([][]interface{}, error) {
  491. rows, err := db.Query(`SELECT Day, Added, Removed, Bounced FROM UserMovement WHERE Day > now() - '1 year'::INTERVAL ORDER BY Day`)
  492. if err != nil {
  493. return nil, err
  494. }
  495. defer rows.Close()
  496. res := [][]interface{}{
  497. {"Day", "Joined", "Left", "Bounced"},
  498. }
  499. for rows.Next() {
  500. var day time.Time
  501. var added, removed, bounced int
  502. err := rows.Scan(&day, &added, &removed, &bounced)
  503. if err != nil {
  504. return nil, err
  505. }
  506. row := []interface{}{day.Format("2006-01-02"), added, -removed, bounced}
  507. if removed == 0 {
  508. row[2] = nil
  509. }
  510. if bounced == 0 {
  511. row[3] = nil
  512. }
  513. res = append(res, row)
  514. }
  515. return res, nil
  516. }