blob: 4d854770bd6ae0898bf9864bf3d7bb31c647bd18 [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"
Serge Bazanski376f5872020-10-30 14:05:04 +010041 "log"
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020042 "net/http"
43 "os"
44 "os/user"
45 "path/filepath"
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020046 "strconv"
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020047 "sync"
48 "time"
49
50 "github.com/golang/glog"
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020051)
52
53var (
54 binaryName = filepath.Base(os.Args[0])
55 binaryHash string
56 hostname string
57 username string
58 serverStart = time.Now()
59
60 lock sync.Mutex
61 sections []section
62 tmpl = template.Must(reparse(nil))
63 funcs = make(template.FuncMap)
64
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020065 GitCommit = "unknown"
66 GitVersion = "unknown"
67 Builder = "unknown"
68 BuildTimestamp = "0"
69 // mirko populates this
70 PublicAddress = "#"
71
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020072 DefaultMux = true
73)
74
75type section struct {
76 Banner string
77 Fragment string
78 F func() interface{}
79}
80
81var statusHTML = `<!DOCTYPE html>
82<html>
83<head>
84<title>Status for {{.BinaryName}}</title>
85<style>
86body {
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020087background: #fff;
88}
89h1 {
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020090font-family: sans-serif;
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020091clear: both;
92width: 100%;
93text-align: center;
94font-size: 120%;
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020095padding-top: 0.3em;
96padding-bottom: 0.3em;
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020097background: #eeeeff;
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020098margin-top: 1em;
Serge Bazanskicc25bdf2018-10-25 14:02:58 +020099}
100.lefthand {
101float: left;
102width: 80%;
103}
104.righthand {
105text-align: right;
106}
107</style>
108</head>
109<h1>Status for {{.BinaryName}}</h1>
110<div>
111<div class=lefthand>
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200112Started: {{.StartTime}} -- up {{.Up}}<br>
113Built on {{.BuildTime}}<br>
114Built at {{.Builder}}<br>
115Built from git checkout <a href="https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/{{.GitCommit}}">{{.GitVersion}}</a><br>
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200116SHA256 {{.BinaryHash}}<br>
117</div>
118<div class=righthand>
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200119Running as {{.Username}} on <a href="{{.PublicAddress}}">{{.Hostname}}</a><br>
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200120Load {{.LoadAvg}}<br>
121View <a href=/debug/status>status</a>,
Sergiusz Bazanski400ac7a2020-01-23 14:17:30 +0100122 <a href=/debug/requests>requests</a>,
123 <a href=/debug/pprof>profile</a>
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200124</div>
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200125<div style="clear: both;"> </div>
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200126</div>`
127
128func reparse(sections []section) (*template.Template, error) {
129 var buf bytes.Buffer
130
131 io.WriteString(&buf, `{{define "status"}}`)
132 io.WriteString(&buf, statusHTML)
133
134 for i, sec := range sections {
135 fmt.Fprintf(&buf, "<h1>%s</h1>\n", html.EscapeString(sec.Banner))
136 fmt.Fprintf(&buf, "{{$sec := index .Sections %d}}\n", i)
137 fmt.Fprintf(&buf, `{{template "sec-%d" call $sec.F}}`+"\n", i)
138 }
139 fmt.Fprintf(&buf, `</html>`)
140 io.WriteString(&buf, "{{end}}\n")
141
142 for i, sec := range sections {
143 fmt.Fprintf(&buf, `{{define "sec-%d"}}%s{{end}}\n`, i, sec.Fragment)
144 }
145 return template.New("").Funcs(funcs).Parse(buf.String())
146}
147
148func StatusHandler(w http.ResponseWriter, r *http.Request) {
149 lock.Lock()
150 defer lock.Unlock()
151
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200152 buildTime := time.Unix(0, 0)
153 if buildTimeNum, err := strconv.Atoi(BuildTimestamp); err == nil {
154 buildTime = time.Unix(int64(buildTimeNum), 0)
155 }
156
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200157 data := struct {
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200158 Sections []section
159 BinaryName string
160 BinaryHash string
161 GitVersion string
162 GitCommit string
163 Builder string
164 BuildTime string
165 Hostname string
166 Username string
167 StartTime string
168 Up string
169 LoadAvg string
170 PublicAddress string
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200171 }{
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200172 Sections: sections,
173 BinaryName: binaryName,
174 BinaryHash: binaryHash,
175 GitVersion: GitVersion,
176 GitCommit: GitCommit,
177 Builder: Builder,
178 BuildTime: fmt.Sprintf("%s (%d)", buildTime.Format(time.RFC1123), buildTime.Unix()),
179 Hostname: hostname,
180 Username: username,
181 StartTime: serverStart.Format(time.RFC1123),
182 Up: time.Since(serverStart).String(),
183 LoadAvg: loadAverage(),
184 PublicAddress: PublicAddress,
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200185 }
186
187 if err := tmpl.ExecuteTemplate(w, "status", data); err != nil {
188 glog.Errorf("servenv: couldn't execute template: %v", err)
189 }
190}
191
192func init() {
193 var err error
194 hostname, err = os.Hostname()
195 if err != nil {
196 glog.Fatalf("os.Hostname: %v", err)
197 }
198
199 user, err := user.Current()
200 if err != nil {
Serge Bazanski376f5872020-10-30 14:05:04 +0100201 log.Printf("user.Current: %v", err)
202 username = "UNKNOWN"
203 } else {
204 username = fmt.Sprintf("%s (%s)", user.Username, user.Uid)
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200205 }
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200206
Serge Bazanski8fab2be2018-10-25 23:37:37 +0200207 exec, err := os.Executable()
208 if err == nil {
209 f, err := os.Open(exec)
210 if err == nil {
211 h := sha256.New()
212 if _, err := io.Copy(h, f); err != nil {
213 glog.Fatalf("io.Copy: %v", err)
214 }
215 binaryHash = fmt.Sprintf("%x", h.Sum(nil))
216 } else {
217 glog.Errorf("Could not get SHA256 of binary: os.Open(%q): %v", exec, err)
218 binaryHash = "could not read executable"
219 }
220 } else {
221 glog.Errorf("Could not get SHA256 of binary: os.Executable(): %v", err)
222 binaryHash = "could not get executable"
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200223 }
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200224
225 if DefaultMux {
226 http.HandleFunc("/debug/status", StatusHandler)
227 }
228}
229
230// AddStatusPart adds a new section to status. frag is used as a
231// subtemplate of the template used to render /debug/status, and will
232// be executed using the value of invoking f at the time of the
233// /debug/status request. frag is parsed and executed with the
234// html/template package. Functions registered with AddStatusFuncs
235// may be used in the template.
236func AddStatusPart(banner, frag string, f func(context.Context) interface{}) {
237 lock.Lock()
238 defer lock.Unlock()
239
240 secs := append(sections, section{
241 Banner: banner,
242 Fragment: frag,
243 F: func() interface{} { return f(context.Background()) },
244 })
245
246 var err error
247 tmpl, err = reparse(secs)
248 if err != nil {
249 secs[len(secs)-1] = section{
250 Banner: banner,
251 Fragment: "<code>bad status template: {{.}}</code>",
252 F: func() interface{} { return err },
253 }
254 }
255 tmpl, _ = reparse(secs)
256 sections = secs
257}
258
259// AddStatusSection registers a function that generates extra
260// information for /debug/status. If banner is not empty, it will be
261// used as a header before the information. If more complex output
262// than a simple string is required use AddStatusPart instead.
263func AddStatusSection(banner string, f func(context.Context) string) {
264 AddStatusPart(banner, `{{.}}`, func(ctx context.Context) interface{} { return f(ctx) })
265}