main.go 12 KB

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