main.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  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.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
  155. err = srv.Serve(listener)
  156. if err != nil {
  157. log.Fatalln("https:", err)
  158. }
  159. }
  160. var (
  161. cacheData []byte
  162. cacheTime time.Time
  163. cacheMut sync.Mutex
  164. )
  165. const maxCacheTime = 5 * 60 * time.Second
  166. func rootHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  167. if r.URL.Path == "/" || r.URL.Path == "/index.html" {
  168. cacheMut.Lock()
  169. defer cacheMut.Unlock()
  170. if time.Since(cacheTime) > maxCacheTime {
  171. rep := getReport(db)
  172. buf := new(bytes.Buffer)
  173. err := tpl.Execute(buf, rep)
  174. if err != nil {
  175. log.Println(err)
  176. http.Error(w, "Template Error", http.StatusInternalServerError)
  177. return
  178. }
  179. cacheData = buf.Bytes()
  180. cacheTime = time.Now()
  181. }
  182. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  183. w.Write(cacheData)
  184. } else {
  185. http.Error(w, "Not found", 404)
  186. return
  187. }
  188. }
  189. func newDataHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  190. defer r.Body.Close()
  191. var rep report
  192. rep.Date = time.Now().UTC().Format("20060102")
  193. lr := &io.LimitedReader{R: r.Body, N: 10240}
  194. if err := json.NewDecoder(lr).Decode(&rep); err != nil {
  195. log.Println("json decode:", err)
  196. http.Error(w, "JSON Decode Error", http.StatusInternalServerError)
  197. return
  198. }
  199. if err := rep.Validate(); err != nil {
  200. log.Println("validate:", err)
  201. log.Printf("%#v", rep)
  202. http.Error(w, "Validation Error", http.StatusInternalServerError)
  203. return
  204. }
  205. if err := insertReport(db, rep); err != nil {
  206. log.Println("insert:", err)
  207. log.Printf("%#v", rep)
  208. http.Error(w, "Database Error", http.StatusInternalServerError)
  209. return
  210. }
  211. }
  212. func summaryHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  213. s, err := getSummary(db)
  214. if err != nil {
  215. log.Println("summaryHandler:", err)
  216. http.Error(w, "Database Error", http.StatusInternalServerError)
  217. return
  218. }
  219. bs, err := s.MarshalJSON()
  220. if err != nil {
  221. log.Println("summaryHandler:", err)
  222. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  223. return
  224. }
  225. w.Header().Set("Content-Type", "application/json")
  226. w.Write(bs)
  227. }
  228. type category struct {
  229. Values [4]float64
  230. Key string
  231. Descr string
  232. Unit string
  233. Binary bool
  234. }
  235. func getReport(db *sql.DB) map[string]interface{} {
  236. nodes := 0
  237. var versions []string
  238. var platforms []string
  239. var oses []string
  240. var numFolders []int
  241. var numDevices []int
  242. var totFiles []int
  243. var maxFiles []int
  244. var totMiB []int
  245. var maxMiB []int
  246. var memoryUsage []int
  247. var sha256Perf []float64
  248. var memorySize []int
  249. var compilers []string
  250. var builders []string
  251. rows, err := db.Query(`SELECT * FROM Reports WHERE Received > now() - '1 day'::INTERVAL`)
  252. if err != nil {
  253. log.Println("sql:", err)
  254. return nil
  255. }
  256. defer rows.Close()
  257. for rows.Next() {
  258. var rep report
  259. err := rows.Scan(&rep.Received, &rep.UniqueID, &rep.Version,
  260. &rep.LongVersion, &rep.Platform, &rep.NumFolders, &rep.NumDevices,
  261. &rep.TotFiles, &rep.FolderMaxFiles, &rep.TotMiB, &rep.FolderMaxMiB,
  262. &rep.MemoryUsageMiB, &rep.SHA256Perf, &rep.MemorySize, &rep.Date)
  263. if err != nil {
  264. log.Println("sql:", err)
  265. return nil
  266. }
  267. nodes++
  268. versions = append(versions, transformVersion(rep.Version))
  269. platforms = append(platforms, rep.Platform)
  270. ps := strings.Split(rep.Platform, "-")
  271. oses = append(oses, ps[0])
  272. if m := compilerRe.FindStringSubmatch(rep.LongVersion); len(m) == 3 {
  273. compilers = append(compilers, m[1])
  274. builders = append(builders, m[2])
  275. }
  276. if rep.NumFolders > 0 {
  277. numFolders = append(numFolders, rep.NumFolders)
  278. }
  279. if rep.NumDevices > 0 {
  280. numDevices = append(numDevices, rep.NumDevices)
  281. }
  282. if rep.TotFiles > 0 {
  283. totFiles = append(totFiles, rep.TotFiles)
  284. }
  285. if rep.FolderMaxFiles > 0 {
  286. maxFiles = append(maxFiles, rep.FolderMaxFiles)
  287. }
  288. if rep.TotMiB > 0 {
  289. totMiB = append(totMiB, rep.TotMiB*(1<<20))
  290. }
  291. if rep.FolderMaxMiB > 0 {
  292. maxMiB = append(maxMiB, rep.FolderMaxMiB*(1<<20))
  293. }
  294. if rep.MemoryUsageMiB > 0 {
  295. memoryUsage = append(memoryUsage, rep.MemoryUsageMiB*(1<<20))
  296. }
  297. if rep.SHA256Perf > 0 {
  298. sha256Perf = append(sha256Perf, rep.SHA256Perf*(1<<20))
  299. }
  300. if rep.MemorySize > 0 {
  301. memorySize = append(memorySize, rep.MemorySize*(1<<20))
  302. }
  303. }
  304. var categories []category
  305. categories = append(categories, category{
  306. Values: statsForInts(totFiles),
  307. Descr: "Files Managed per Device",
  308. })
  309. categories = append(categories, category{
  310. Values: statsForInts(maxFiles),
  311. Descr: "Files in Largest Folder",
  312. })
  313. categories = append(categories, category{
  314. Values: statsForInts(totMiB),
  315. Descr: "Data Managed per Device",
  316. Unit: "B",
  317. Binary: true,
  318. })
  319. categories = append(categories, category{
  320. Values: statsForInts(maxMiB),
  321. Descr: "Data in Largest Folder",
  322. Unit: "B",
  323. Binary: true,
  324. })
  325. categories = append(categories, category{
  326. Values: statsForInts(numDevices),
  327. Descr: "Number of Devices in Cluster",
  328. })
  329. categories = append(categories, category{
  330. Values: statsForInts(numFolders),
  331. Descr: "Number of Folders Configured",
  332. })
  333. categories = append(categories, category{
  334. Values: statsForInts(memoryUsage),
  335. Descr: "Memory Usage",
  336. Unit: "B",
  337. Binary: true,
  338. })
  339. categories = append(categories, category{
  340. Values: statsForInts(memorySize),
  341. Descr: "System Memory",
  342. Unit: "B",
  343. Binary: true,
  344. })
  345. categories = append(categories, category{
  346. Values: statsForFloats(sha256Perf),
  347. Descr: "SHA-256 Hashing Performance",
  348. Unit: "B/s",
  349. Binary: true,
  350. })
  351. r := make(map[string]interface{})
  352. r["nodes"] = nodes
  353. r["categories"] = categories
  354. r["versions"] = analyticsFor(versions, 10)
  355. r["platforms"] = analyticsFor(platforms, 0)
  356. r["os"] = analyticsFor(oses, 0)
  357. r["compilers"] = analyticsFor(compilers, 12)
  358. r["builders"] = analyticsFor(builders, 12)
  359. return r
  360. }
  361. func ensureDir(dir string, mode int) {
  362. fi, err := os.Stat(dir)
  363. if os.IsNotExist(err) {
  364. os.MkdirAll(dir, 0700)
  365. } else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
  366. os.Chmod(dir, os.FileMode(mode))
  367. }
  368. }
  369. var vRe = regexp.MustCompile(`^(v\d+\.\d+\.\d+(?:-[a-z]\w+)?)[+\.-]`)
  370. // transformVersion returns a version number formatted correctly, with all
  371. // development versions aggregated into one.
  372. func transformVersion(v string) string {
  373. if v == "unknown-dev" {
  374. return v
  375. }
  376. if !strings.HasPrefix(v, "v") {
  377. v = "v" + v
  378. }
  379. if m := vRe.FindStringSubmatch(v); len(m) > 0 {
  380. return m[1] + " (+dev)"
  381. }
  382. // Truncate old versions to just the generation part
  383. for _, agg := range aggregateVersions {
  384. if strings.HasPrefix(v, agg) {
  385. return agg + ".x"
  386. }
  387. }
  388. return v
  389. }
  390. type summary struct {
  391. versions map[string]int // version string to count index
  392. rows map[string][]int // date to list of counts
  393. }
  394. func newSummary() summary {
  395. return summary{
  396. versions: make(map[string]int),
  397. rows: make(map[string][]int),
  398. }
  399. }
  400. func (s *summary) setCount(date, version string, count int) {
  401. idx, ok := s.versions[version]
  402. if !ok {
  403. idx = len(s.versions)
  404. s.versions[version] = idx
  405. }
  406. row := s.rows[date]
  407. if len(row) <= idx {
  408. old := row
  409. row = make([]int, idx+1)
  410. copy(row, old)
  411. s.rows[date] = row
  412. }
  413. row[idx] = count
  414. }
  415. func (s *summary) MarshalJSON() ([]byte, error) {
  416. var versions []string
  417. for v := range s.versions {
  418. versions = append(versions, v)
  419. }
  420. sort.Strings(versions)
  421. headerRow := []interface{}{"Day"}
  422. for _, v := range versions {
  423. headerRow = append(headerRow, v)
  424. }
  425. var table [][]interface{}
  426. table = append(table, headerRow)
  427. var dates []string
  428. for k := range s.rows {
  429. dates = append(dates, k)
  430. }
  431. sort.Strings(dates)
  432. for _, date := range dates {
  433. row := []interface{}{date}
  434. for _, ver := range versions {
  435. idx := s.versions[ver]
  436. if len(s.rows[date]) > idx && s.rows[date][idx] > 0 {
  437. row = append(row, s.rows[date][idx])
  438. } else {
  439. row = append(row, nil)
  440. }
  441. }
  442. table = append(table, row)
  443. }
  444. return json.Marshal(table)
  445. }
  446. func getSummary(db *sql.DB) (summary, error) {
  447. s := newSummary()
  448. rows, err := db.Query(`SELECT Day, Version, Count FROM VersionSummary WHERE Day > now() - '1 year'::INTERVAL;`)
  449. if err != nil {
  450. return summary{}, err
  451. }
  452. defer rows.Close()
  453. for rows.Next() {
  454. var day time.Time
  455. var ver string
  456. var num int
  457. err := rows.Scan(&day, &ver, &num)
  458. if err != nil {
  459. return summary{}, err
  460. }
  461. if ver == "v0.0" {
  462. // ?
  463. continue
  464. }
  465. // SUPER UGLY HACK to avoid having to do sorting properly
  466. if len(ver) == 4 { // v0.x
  467. ver = ver[:3] + "0" + ver[3:] // now v0.0x
  468. }
  469. s.setCount(day.Format("2006-01-02"), ver, num)
  470. }
  471. return s, nil
  472. }