blob: 0f3f0042053a28a66e0f825c8e802f0a8e6c25bd [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"encoding/json"
"fmt"
"math"
"net/http"
"path/filepath"
"strings"
"text/template"
"time"
"v.io/jiri"
"v.io/x/devtools/internal/cache"
)
const (
statusBucket = "gs://vanadium-oncall/data"
)
const (
hourDivWidth = 5
)
var statusPageTemplate = template.Must(template.New("status").Funcs(statusFuncMap).Parse(`
{{ $days := makeSlice 0 1 2 3 4 5 6 }}
<!DOCTYPE html>
<html>
<head>
<title>Vanadium Services Status</title>
<link href="//fonts.googleapis.com/css?family=Source+Code+Pro:400,500|Roboto:500,400italic,300,500italic,300italic,400" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/status.css">
<script type="text/javascript" src="/static/status.js"></script>
</head>
<body>
<div id="incident-details"></div>
<div class="header">
<div class="title">
Vanadium Services Status
</div>
</div>
<div class="main-container">
<div class="main">
<div class="header-row">
<div class="header-current">Current</div>
<div class="header-history">
{{ range $dayIndex := $days }}
<div class="day-label" style="left: {{ offsetPxForDay $.CollectionTimestamp $dayIndex }}">
{{ dayLabel $.CollectionTimestamp $dayIndex }}
</div>
{{ end }}
</div>
</div>
{{ range $serviceData := .Status }}
<div class="service-row">
<div class="service-header">
<div class="service-name">{{ $serviceData.Name }}</div>
<div class="service-buildts">Built at: {{ $serviceData.BuildTimestamp }}</div>
<div class="service-snapshot">Snapshot: {{ $serviceData.SnapshotLabel }}</div>
</div>
<div class="service-cur-status {{ $serviceData.CurrentStatus }}">
</div>
<div style="width: {{ offsetPxForDay $.CollectionTimestamp 7 }}" class="service-history">
{{ range $dayIndex2 := $days }}
<div class="day-divider" style="left: {{ offsetPxForDay $.CollectionTimestamp $dayIndex2 }}">
</div>
{{ end }}
{{ range $incidentData := $serviceData.Incidents}}
<div class="service-history-item {{ $incidentData.Status }}"
style="left: {{ offsetPx $.CollectionTimestamp $incidentData.Start }};
width: {{ widthPxForDuration $incidentData.Duration }}"
onmouseover="mouseOverIncidentItem(this, '{{ incidentDetails $incidentData.Start $incidentData.Duration }}')"
onmouseout="mouseOutIncidentItem()">
</div>
{{ end }}
</div>
</div>
{{ end }}
</div>
</div>
</body>
</html>
`))
var statusFuncMap = template.FuncMap{
"dayLabel": dayLabel,
"incidentDetails": incidentDetails,
"makeSlice": makeSlice,
"offsetPx": offsetPx,
"offsetPxForDay": offsetPxForDay,
"widthPxForDuration": widthPxForDuration,
}
func dayLabel(collectionTime int64, dayIndex int) string {
t := time.Unix(collectionTime, 0)
roundedTime := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.Local)
return time.Unix(roundedTime.Unix()-int64((dayIndex+1)*24*3600), 0).Format("Jan 02")
}
func incidentDetails(startTime, duration int64) string {
return fmt.Sprintf("%s - %s", time.Unix(startTime, 0).Format("2006-01-02 15:04"), time.Unix(startTime+duration, 0).Format("2006-01-02 15:04"))
}
func makeSlice(args ...interface{}) []interface{} {
return args
}
func offsetPx(collectionTime, curTime int64) string {
offsetHours := float32(collectionTime-curTime) / 3600.0
return fmt.Sprintf("%fpx", offsetHours*hourDivWidth)
}
func offsetPxForDay(collectionTime int64, dayIndex int) string {
t := time.Unix(collectionTime, 0)
roundedTime := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.Local)
return offsetPx(collectionTime, roundedTime.Unix()-int64(dayIndex*24*3600))
}
func widthPxForDuration(duration int64) string {
// 2px Minimum width.
width := math.Max(2, float64(duration)/3600.0*hourDivWidth)
return fmt.Sprintf("%fpx", width)
}
func displayServiceStatusPage(jirix *jiri.X, w http.ResponseWriter, r *http.Request) (e error) {
s := jirix.NewSeq()
// Set up the root directory.
root := cacheFlag
if root == "" {
tmpDir, err := s.TempDir("", "")
if err != nil {
return err
}
defer jirix.NewSeq().RemoveAll(tmpDir)
root = tmpDir
}
root = filepath.Join(root, "status")
var out bytes.Buffer
// Read timestamp from the "latest" file.
if err := s.MkdirAll(root, 0700).
Capture(&out, &out).
Last("gsutil", "-q", "cat", statusBucketFlag+"/latest"); err != nil {
return err
}
// Read status file.
cachedFile, err := cache.StoreGoogleStorageFile(jirix, root, statusBucketFlag, out.String()+".status")
if err != nil {
return err
}
fileBytes, err := s.ReadFile(cachedFile)
if err != nil {
return err
}
// Parse status file and render.
type statusData struct {
Name string
BuildTimestamp string
SnapshotLabel string
CurrentStatus string
Incidents []struct {
Start int64
Duration int64
Status string
}
}
var data struct {
CollectionTimestamp int64
Status []statusData
}
if err := json.Unmarshal(fileBytes, &data); err != nil {
return fmt.Errorf("Unmarshal(%v) failed: %v", string(fileBytes), err)
}
filteredStatus := []statusData{}
for _, s := range data.Status {
// Ignore application and binary repo.
if s.Name == "application repository" || s.Name == "binary repository" {
continue
}
s.Name = strings.ToUpper(s.Name)
if s.BuildTimestamp == "" {
s.BuildTimestamp = "N/A"
}
if s.SnapshotLabel == "" {
s.SnapshotLabel = "N/A"
}
filteredStatus = append(filteredStatus, s)
}
data.Status = filteredStatus
if err := statusPageTemplate.Execute(w, data); err != nil {
return fmt.Errorf("Execute() failed: %v!!", err)
}
return nil
}