blob: 20d5151062a0c1f3cdb280e922a7f4d63ce8342d [file] [log] [blame]
Serge Bazanskicc25bdf2018-10-25 14:02:58 +02001// Copyright 2018 Serge Bazanski
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Based off github.com/youtube/doorman/blob/master/go/status/status.go
16
17// Copyright 2016 Google, Inc.
18//
19// Licensed under the Apache License, Version 2.0 (the "License");
20// you may not use this file except in compliance with the License.
21// You may obtain a copy of the License at
22//
23// http://www.apache.org/licenses/LICENSE-2.0
24//
25// Unless required by applicable law or agreed to in writing, software
26// distributed under the License is distributed on an "AS IS" BASIS,
27// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28// See the License for the specific language governing permissions and
29// limitations under the License.
30
31package statusz
32
33import (
34 "bytes"
35 "context"
36 "crypto/sha256"
37 "fmt"
38 "html"
39 "html/template"
40 "io"
41 "net/http"
42 "os"
43 "os/user"
44 "path/filepath"
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020045 "strconv"
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020046 "sync"
47 "time"
48
49 "github.com/golang/glog"
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020050)
51
52var (
53 binaryName = filepath.Base(os.Args[0])
54 binaryHash string
55 hostname string
56 username string
57 serverStart = time.Now()
58
59 lock sync.Mutex
60 sections []section
61 tmpl = template.Must(reparse(nil))
62 funcs = make(template.FuncMap)
63
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020064 GitCommit = "unknown"
65 GitVersion = "unknown"
66 Builder = "unknown"
67 BuildTimestamp = "0"
68 // mirko populates this
69 PublicAddress = "#"
70
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020071 DefaultMux = true
72)
73
74type section struct {
75 Banner string
76 Fragment string
77 F func() interface{}
78}
79
80var statusHTML = `<!DOCTYPE html>
81<html>
82<head>
83<title>Status for {{.BinaryName}}</title>
84<style>
85body {
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020086background: #fff;
87}
88h1 {
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020089font-family: sans-serif;
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020090clear: both;
91width: 100%;
92text-align: center;
93font-size: 120%;
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020094padding-top: 0.3em;
95padding-bottom: 0.3em;
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020096background: #eeeeff;
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020097margin-top: 1em;
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020098}
99.lefthand {
100float: left;
101width: 80%;
102}
103.righthand {
104text-align: right;
105}
106</style>
107</head>
108<h1>Status for {{.BinaryName}}</h1>
109<div>
110<div class=lefthand>
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200111Started: {{.StartTime}} -- up {{.Up}}<br>
112Built on {{.BuildTime}}<br>
113Built at {{.Builder}}<br>
114Built from git checkout <a href="https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/{{.GitCommit}}">{{.GitVersion}}</a><br>
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200115SHA256 {{.BinaryHash}}<br>
116</div>
117<div class=righthand>
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200118Running as {{.Username}} on <a href="{{.PublicAddress}}">{{.Hostname}}</a><br>
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200119Load {{.LoadAvg}}<br>
120View <a href=/debug/status>status</a>,
121 <a href=/debug/requests>requests</a>
122</div>
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200123<div style="clear: both;"> </div>
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200124</div>`
125
126func reparse(sections []section) (*template.Template, error) {
127 var buf bytes.Buffer
128
129 io.WriteString(&buf, `{{define "status"}}`)
130 io.WriteString(&buf, statusHTML)
131
132 for i, sec := range sections {
133 fmt.Fprintf(&buf, "<h1>%s</h1>\n", html.EscapeString(sec.Banner))
134 fmt.Fprintf(&buf, "{{$sec := index .Sections %d}}\n", i)
135 fmt.Fprintf(&buf, `{{template "sec-%d" call $sec.F}}`+"\n", i)
136 }
137 fmt.Fprintf(&buf, `</html>`)
138 io.WriteString(&buf, "{{end}}\n")
139
140 for i, sec := range sections {
141 fmt.Fprintf(&buf, `{{define "sec-%d"}}%s{{end}}\n`, i, sec.Fragment)
142 }
143 return template.New("").Funcs(funcs).Parse(buf.String())
144}
145
146func StatusHandler(w http.ResponseWriter, r *http.Request) {
147 lock.Lock()
148 defer lock.Unlock()
149
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200150 buildTime := time.Unix(0, 0)
151 if buildTimeNum, err := strconv.Atoi(BuildTimestamp); err == nil {
152 buildTime = time.Unix(int64(buildTimeNum), 0)
153 }
154
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200155 data := struct {
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200156 Sections []section
157 BinaryName string
158 BinaryHash string
159 GitVersion string
160 GitCommit string
161 Builder string
162 BuildTime string
163 Hostname string
164 Username string
165 StartTime string
166 Up string
167 LoadAvg string
168 PublicAddress string
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200169 }{
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200170 Sections: sections,
171 BinaryName: binaryName,
172 BinaryHash: binaryHash,
173 GitVersion: GitVersion,
174 GitCommit: GitCommit,
175 Builder: Builder,
176 BuildTime: fmt.Sprintf("%s (%d)", buildTime.Format(time.RFC1123), buildTime.Unix()),
177 Hostname: hostname,
178 Username: username,
179 StartTime: serverStart.Format(time.RFC1123),
180 Up: time.Since(serverStart).String(),
181 LoadAvg: loadAverage(),
182 PublicAddress: PublicAddress,
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200183 }
184
185 if err := tmpl.ExecuteTemplate(w, "status", data); err != nil {
186 glog.Errorf("servenv: couldn't execute template: %v", err)
187 }
188}
189
190func init() {
191 var err error
192 hostname, err = os.Hostname()
193 if err != nil {
194 glog.Fatalf("os.Hostname: %v", err)
195 }
196
197 user, err := user.Current()
198 if err != nil {
199 glog.Fatalf("user.Current: %v", err)
200 }
201 username = fmt.Sprintf("%s (%s)", user.Username, user.Uid)
202
Serge Bazanski8fab2be2018-10-25 23:37:37 +0200203 exec, err := os.Executable()
204 if err == nil {
205 f, err := os.Open(exec)
206 if err == nil {
207 h := sha256.New()
208 if _, err := io.Copy(h, f); err != nil {
209 glog.Fatalf("io.Copy: %v", err)
210 }
211 binaryHash = fmt.Sprintf("%x", h.Sum(nil))
212 } else {
213 glog.Errorf("Could not get SHA256 of binary: os.Open(%q): %v", exec, err)
214 binaryHash = "could not read executable"
215 }
216 } else {
217 glog.Errorf("Could not get SHA256 of binary: os.Executable(): %v", err)
218 binaryHash = "could not get executable"
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200219 }
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200220
221 if DefaultMux {
222 http.HandleFunc("/debug/status", StatusHandler)
223 }
224}
225
226// AddStatusPart adds a new section to status. frag is used as a
227// subtemplate of the template used to render /debug/status, and will
228// be executed using the value of invoking f at the time of the
229// /debug/status request. frag is parsed and executed with the
230// html/template package. Functions registered with AddStatusFuncs
231// may be used in the template.
232func AddStatusPart(banner, frag string, f func(context.Context) interface{}) {
233 lock.Lock()
234 defer lock.Unlock()
235
236 secs := append(sections, section{
237 Banner: banner,
238 Fragment: frag,
239 F: func() interface{} { return f(context.Background()) },
240 })
241
242 var err error
243 tmpl, err = reparse(secs)
244 if err != nil {
245 secs[len(secs)-1] = section{
246 Banner: banner,
247 Fragment: "<code>bad status template: {{.}}</code>",
248 F: func() interface{} { return err },
249 }
250 }
251 tmpl, _ = reparse(secs)
252 sections = secs
253}
254
255// AddStatusSection registers a function that generates extra
256// information for /debug/status. If banner is not empty, it will be
257// used as a header before the information. If more complex output
258// than a simple string is required use AddStatusPart instead.
259func AddStatusSection(banner string, f func(context.Context) string) {
260 AddStatusPart(banner, `{{.}}`, func(ctx context.Context) interface{} { return f(ctx) })
261}