blob: 4d854770bd6ae0898bf9864bf3d7bb31c647bd18 [file] [log] [blame]
// Copyright 2018 Serge Bazanski
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Based off github.com/youtube/doorman/blob/master/go/status/status.go
// Copyright 2016 Google, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package statusz
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"html"
"html/template"
"io"
"log"
"net/http"
"os"
"os/user"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/golang/glog"
)
var (
binaryName = filepath.Base(os.Args[0])
binaryHash string
hostname string
username string
serverStart = time.Now()
lock sync.Mutex
sections []section
tmpl = template.Must(reparse(nil))
funcs = make(template.FuncMap)
GitCommit = "unknown"
GitVersion = "unknown"
Builder = "unknown"
BuildTimestamp = "0"
// mirko populates this
PublicAddress = "#"
DefaultMux = true
)
type section struct {
Banner string
Fragment string
F func() interface{}
}
var statusHTML = `<!DOCTYPE html>
<html>
<head>
<title>Status for {{.BinaryName}}</title>
<style>
body {
background: #fff;
}
h1 {
font-family: sans-serif;
clear: both;
width: 100%;
text-align: center;
font-size: 120%;
padding-top: 0.3em;
padding-bottom: 0.3em;
background: #eeeeff;
margin-top: 1em;
}
.lefthand {
float: left;
width: 80%;
}
.righthand {
text-align: right;
}
</style>
</head>
<h1>Status for {{.BinaryName}}</h1>
<div>
<div class=lefthand>
Started: {{.StartTime}} -- up {{.Up}}<br>
Built on {{.BuildTime}}<br>
Built at {{.Builder}}<br>
Built from git checkout <a href="https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/{{.GitCommit}}">{{.GitVersion}}</a><br>
SHA256 {{.BinaryHash}}<br>
</div>
<div class=righthand>
Running as {{.Username}} on <a href="{{.PublicAddress}}">{{.Hostname}}</a><br>
Load {{.LoadAvg}}<br>
View <a href=/debug/status>status</a>,
<a href=/debug/requests>requests</a>,
<a href=/debug/pprof>profile</a>
</div>
<div style="clear: both;"> </div>
</div>`
func reparse(sections []section) (*template.Template, error) {
var buf bytes.Buffer
io.WriteString(&buf, `{{define "status"}}`)
io.WriteString(&buf, statusHTML)
for i, sec := range sections {
fmt.Fprintf(&buf, "<h1>%s</h1>\n", html.EscapeString(sec.Banner))
fmt.Fprintf(&buf, "{{$sec := index .Sections %d}}\n", i)
fmt.Fprintf(&buf, `{{template "sec-%d" call $sec.F}}`+"\n", i)
}
fmt.Fprintf(&buf, `</html>`)
io.WriteString(&buf, "{{end}}\n")
for i, sec := range sections {
fmt.Fprintf(&buf, `{{define "sec-%d"}}%s{{end}}\n`, i, sec.Fragment)
}
return template.New("").Funcs(funcs).Parse(buf.String())
}
func StatusHandler(w http.ResponseWriter, r *http.Request) {
lock.Lock()
defer lock.Unlock()
buildTime := time.Unix(0, 0)
if buildTimeNum, err := strconv.Atoi(BuildTimestamp); err == nil {
buildTime = time.Unix(int64(buildTimeNum), 0)
}
data := struct {
Sections []section
BinaryName string
BinaryHash string
GitVersion string
GitCommit string
Builder string
BuildTime string
Hostname string
Username string
StartTime string
Up string
LoadAvg string
PublicAddress string
}{
Sections: sections,
BinaryName: binaryName,
BinaryHash: binaryHash,
GitVersion: GitVersion,
GitCommit: GitCommit,
Builder: Builder,
BuildTime: fmt.Sprintf("%s (%d)", buildTime.Format(time.RFC1123), buildTime.Unix()),
Hostname: hostname,
Username: username,
StartTime: serverStart.Format(time.RFC1123),
Up: time.Since(serverStart).String(),
LoadAvg: loadAverage(),
PublicAddress: PublicAddress,
}
if err := tmpl.ExecuteTemplate(w, "status", data); err != nil {
glog.Errorf("servenv: couldn't execute template: %v", err)
}
}
func init() {
var err error
hostname, err = os.Hostname()
if err != nil {
glog.Fatalf("os.Hostname: %v", err)
}
user, err := user.Current()
if err != nil {
log.Printf("user.Current: %v", err)
username = "UNKNOWN"
} else {
username = fmt.Sprintf("%s (%s)", user.Username, user.Uid)
}
exec, err := os.Executable()
if err == nil {
f, err := os.Open(exec)
if err == nil {
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
glog.Fatalf("io.Copy: %v", err)
}
binaryHash = fmt.Sprintf("%x", h.Sum(nil))
} else {
glog.Errorf("Could not get SHA256 of binary: os.Open(%q): %v", exec, err)
binaryHash = "could not read executable"
}
} else {
glog.Errorf("Could not get SHA256 of binary: os.Executable(): %v", err)
binaryHash = "could not get executable"
}
if DefaultMux {
http.HandleFunc("/debug/status", StatusHandler)
}
}
// AddStatusPart adds a new section to status. frag is used as a
// subtemplate of the template used to render /debug/status, and will
// be executed using the value of invoking f at the time of the
// /debug/status request. frag is parsed and executed with the
// html/template package. Functions registered with AddStatusFuncs
// may be used in the template.
func AddStatusPart(banner, frag string, f func(context.Context) interface{}) {
lock.Lock()
defer lock.Unlock()
secs := append(sections, section{
Banner: banner,
Fragment: frag,
F: func() interface{} { return f(context.Background()) },
})
var err error
tmpl, err = reparse(secs)
if err != nil {
secs[len(secs)-1] = section{
Banner: banner,
Fragment: "<code>bad status template: {{.}}</code>",
F: func() interface{} { return err },
}
}
tmpl, _ = reparse(secs)
sections = secs
}
// AddStatusSection registers a function that generates extra
// information for /debug/status. If banner is not empty, it will be
// used as a header before the information. If more complex output
// than a simple string is required use AddStatusPart instead.
func AddStatusSection(banner string, f func(context.Context) string) {
AddStatusPart(banner, `{{.}}`, func(ctx context.Context) interface{} { return f(ctx) })
}