| // 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" |
| "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 { |
| glog.Fatalf("user.Current: %v", err) |
| } |
| 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) }) |
| } |