blob: 6c0fe1758547a2d6a436d18682c2cb7dad771c13 [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"
45 "sync"
46 "time"
47
48 "github.com/golang/glog"
49 "github.com/shirou/gopsutil/load"
50)
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
64 DefaultMux = true
65)
66
67type section struct {
68 Banner string
69 Fragment string
70 F func() interface{}
71}
72
73var statusHTML = `<!DOCTYPE html>
74<html>
75<head>
76<title>Status for {{.BinaryName}}</title>
77<style>
78body {
79font-family: sans-serif;
80background: #fff;
81}
82h1 {
83clear: both;
84width: 100%;
85text-align: center;
86font-size: 120%;
87background: #eeeeff;
88}
89.lefthand {
90float: left;
91width: 80%;
92}
93.righthand {
94text-align: right;
95}
96</style>
97</head>
98<h1>Status for {{.BinaryName}}</h1>
99<div>
100<div class=lefthand>
101Started at {{.StartTime}}<br>
102Current time {{.CurrentTime}}<br>
103SHA256 {{.BinaryHash}}<br>
104</div>
105<div class=righthand>
106Running as {{.Username}} on {{.Hostname}}<br>
107Load {{.LoadAvg}}<br>
108View <a href=/debug/status>status</a>,
109 <a href=/debug/requests>requests</a>
110</div>
111</div>`
112
113func reparse(sections []section) (*template.Template, error) {
114 var buf bytes.Buffer
115
116 io.WriteString(&buf, `{{define "status"}}`)
117 io.WriteString(&buf, statusHTML)
118
119 for i, sec := range sections {
120 fmt.Fprintf(&buf, "<h1>%s</h1>\n", html.EscapeString(sec.Banner))
121 fmt.Fprintf(&buf, "{{$sec := index .Sections %d}}\n", i)
122 fmt.Fprintf(&buf, `{{template "sec-%d" call $sec.F}}`+"\n", i)
123 }
124 fmt.Fprintf(&buf, `</html>`)
125 io.WriteString(&buf, "{{end}}\n")
126
127 for i, sec := range sections {
128 fmt.Fprintf(&buf, `{{define "sec-%d"}}%s{{end}}\n`, i, sec.Fragment)
129 }
130 return template.New("").Funcs(funcs).Parse(buf.String())
131}
132
133func StatusHandler(w http.ResponseWriter, r *http.Request) {
134 lock.Lock()
135 defer lock.Unlock()
136
137 loadavg := "unknown"
138 l, err := load.AvgWithContext(r.Context())
139 if err == nil {
140 loadavg = fmt.Sprintf("%.2f %.2f %.2f", l.Load1, l.Load5, l.Load15)
141 }
142
143 data := struct {
144 Sections []section
145 BinaryName string
146 BinaryHash string
147 Hostname string
148 Username string
149 StartTime string
150 CurrentTime string
151 LoadAvg string
152 }{
153 Sections: sections,
154 BinaryName: binaryName,
155 BinaryHash: binaryHash,
156 Hostname: hostname,
157 Username: username,
158 StartTime: serverStart.Format(time.RFC1123),
159 CurrentTime: time.Now().Format(time.RFC1123),
160 LoadAvg: loadavg,
161 }
162
163 if err := tmpl.ExecuteTemplate(w, "status", data); err != nil {
164 glog.Errorf("servenv: couldn't execute template: %v", err)
165 }
166}
167
168func init() {
169 var err error
170 hostname, err = os.Hostname()
171 if err != nil {
172 glog.Fatalf("os.Hostname: %v", err)
173 }
174
175 user, err := user.Current()
176 if err != nil {
177 glog.Fatalf("user.Current: %v", err)
178 }
179 username = fmt.Sprintf("%s (%s)", user.Username, user.Uid)
180
Serge Bazanski8fab2be2018-10-25 23:37:37 +0200181 exec, err := os.Executable()
182 if err == nil {
183 f, err := os.Open(exec)
184 if err == nil {
185 h := sha256.New()
186 if _, err := io.Copy(h, f); err != nil {
187 glog.Fatalf("io.Copy: %v", err)
188 }
189 binaryHash = fmt.Sprintf("%x", h.Sum(nil))
190 } else {
191 glog.Errorf("Could not get SHA256 of binary: os.Open(%q): %v", exec, err)
192 binaryHash = "could not read executable"
193 }
194 } else {
195 glog.Errorf("Could not get SHA256 of binary: os.Executable(): %v", err)
196 binaryHash = "could not get executable"
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200197 }
Serge Bazanskicc25bdf2018-10-25 14:02:58 +0200198
199 if DefaultMux {
200 http.HandleFunc("/debug/status", StatusHandler)
201 }
202}
203
204// AddStatusPart adds a new section to status. frag is used as a
205// subtemplate of the template used to render /debug/status, and will
206// be executed using the value of invoking f at the time of the
207// /debug/status request. frag is parsed and executed with the
208// html/template package. Functions registered with AddStatusFuncs
209// may be used in the template.
210func AddStatusPart(banner, frag string, f func(context.Context) interface{}) {
211 lock.Lock()
212 defer lock.Unlock()
213
214 secs := append(sections, section{
215 Banner: banner,
216 Fragment: frag,
217 F: func() interface{} { return f(context.Background()) },
218 })
219
220 var err error
221 tmpl, err = reparse(secs)
222 if err != nil {
223 secs[len(secs)-1] = section{
224 Banner: banner,
225 Fragment: "<code>bad status template: {{.}}</code>",
226 F: func() interface{} { return err },
227 }
228 }
229 tmpl, _ = reparse(secs)
230 sections = secs
231}
232
233// AddStatusSection registers a function that generates extra
234// information for /debug/status. If banner is not empty, it will be
235// used as a header before the information. If more complex output
236// than a simple string is required use AddStatusPart instead.
237func AddStatusSection(banner string, f func(context.Context) string) {
238 AddStatusPart(banner, `{{.}}`, func(ctx context.Context) interface{} { return f(ctx) })
239}