|
| 1 | +// Copyright 2020-2026 Buf Technologies, Inc. |
| 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 | +package lspserve |
| 16 | + |
| 17 | +import ( |
| 18 | + "fmt" |
| 19 | + "html/template" |
| 20 | + "net" |
| 21 | + "net/http" |
| 22 | + "net/http/pprof" |
| 23 | + "os" |
| 24 | + "runtime" |
| 25 | + "runtime/debug" |
| 26 | + "strconv" |
| 27 | + "time" |
| 28 | + |
| 29 | + "github.com/bufbuild/buf/private/pkg/transport/http/httpserver" |
| 30 | +) |
| 31 | + |
| 32 | +// debugServer is an HTTP server that serves debug information about the |
| 33 | +// running LSP server. |
| 34 | +type debugServer struct { |
| 35 | + version string |
| 36 | + startTime time.Time |
| 37 | + |
| 38 | + listener net.Listener |
| 39 | + server *http.Server |
| 40 | +} |
| 41 | + |
| 42 | +// newDebugServer creates and starts a debug HTTP server on the given address. |
| 43 | +// The address format is "host:port", e.g. "localhost:6060" or ":0" for an |
| 44 | +// OS-assigned port. |
| 45 | +func newDebugServer(addr string, version string) (*debugServer, error) { |
| 46 | + listener, err := net.Listen("tcp", addr) |
| 47 | + if err != nil { |
| 48 | + return nil, fmt.Errorf("could not start debug server: %w", err) |
| 49 | + } |
| 50 | + |
| 51 | + // Sample 1-in-1000 blocking events and 1-in-10 mutex contention events. |
| 52 | + // Rate 1 would trace every event and add measurable overhead to the LSP. |
| 53 | + runtime.SetBlockProfileRate(1000) |
| 54 | + runtime.SetMutexProfileFraction(10) |
| 55 | + |
| 56 | + ds := &debugServer{ |
| 57 | + version: version, |
| 58 | + startTime: time.Now(), |
| 59 | + listener: listener, |
| 60 | + } |
| 61 | + |
| 62 | + mux := http.NewServeMux() |
| 63 | + mux.HandleFunc("/", ds.render(mainTmpl, ds.getMain)) |
| 64 | + mux.HandleFunc("/info", ds.render(infoTmpl, ds.getInfo)) |
| 65 | + mux.HandleFunc("/memory", ds.render(memoryTmpl, ds.getMemory)) |
| 66 | + mux.HandleFunc("/debug/pprof/", pprof.Index) |
| 67 | + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) |
| 68 | + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) |
| 69 | + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) |
| 70 | + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) |
| 71 | + |
| 72 | + ds.server = &http.Server{ |
| 73 | + Handler: mux, |
| 74 | + ReadHeaderTimeout: httpserver.DefaultReadHeaderTimeout, |
| 75 | + IdleTimeout: httpserver.DefaultIdleTimeout, |
| 76 | + } |
| 77 | + go func() { _ = ds.server.Serve(listener) }() |
| 78 | + |
| 79 | + return ds, nil |
| 80 | +} |
| 81 | + |
| 82 | +// Addr returns the address the debug server is listening on. |
| 83 | +func (ds *debugServer) Addr() net.Addr { |
| 84 | + return ds.listener.Addr() |
| 85 | +} |
| 86 | + |
| 87 | +// Close shuts down the debug server. |
| 88 | +func (ds *debugServer) Close() error { |
| 89 | + return ds.server.Close() |
| 90 | +} |
| 91 | + |
| 92 | +type serverInfo struct { |
| 93 | + Version string |
| 94 | + StartTime time.Time |
| 95 | + Uptime string |
| 96 | + PID int |
| 97 | + GoVersion string |
| 98 | + GOOS string |
| 99 | + GOARCH string |
| 100 | + NumCPU int |
| 101 | + GOMAXPROCS int |
| 102 | + NumGoroutine int |
| 103 | + BuildInfo string |
| 104 | +} |
| 105 | + |
| 106 | +func (ds *debugServer) getServerInfo() serverInfo { |
| 107 | + uptime := time.Since(ds.startTime).Truncate(time.Second) |
| 108 | + var buildInfoStr string |
| 109 | + if bi, ok := debug.ReadBuildInfo(); ok { |
| 110 | + buildInfoStr = bi.String() |
| 111 | + } |
| 112 | + return serverInfo{ |
| 113 | + Version: ds.version, |
| 114 | + GoVersion: runtime.Version(), |
| 115 | + GOOS: runtime.GOOS, |
| 116 | + GOARCH: runtime.GOARCH, |
| 117 | + PID: os.Getpid(), |
| 118 | + StartTime: ds.startTime, |
| 119 | + Uptime: uptime.String(), |
| 120 | + NumCPU: runtime.NumCPU(), |
| 121 | + GOMAXPROCS: runtime.GOMAXPROCS(0), |
| 122 | + NumGoroutine: runtime.NumGoroutine(), |
| 123 | + BuildInfo: buildInfoStr, |
| 124 | + } |
| 125 | +} |
| 126 | + |
| 127 | +func (ds *debugServer) getMain(_ *http.Request) any { |
| 128 | + return ds.getServerInfo() |
| 129 | +} |
| 130 | + |
| 131 | +func (ds *debugServer) getInfo(_ *http.Request) any { |
| 132 | + return ds.getServerInfo() |
| 133 | +} |
| 134 | + |
| 135 | +func (ds *debugServer) getMemory(_ *http.Request) any { |
| 136 | + var m runtime.MemStats |
| 137 | + runtime.ReadMemStats(&m) |
| 138 | + return m |
| 139 | +} |
| 140 | + |
| 141 | +type dataFunc func(*http.Request) any |
| 142 | + |
| 143 | +func (ds *debugServer) render(tmpl *template.Template, fun dataFunc) http.HandlerFunc { |
| 144 | + return func(w http.ResponseWriter, r *http.Request) { |
| 145 | + if err := tmpl.Execute(w, fun(r)); err != nil { |
| 146 | + http.Error(w, err.Error(), http.StatusInternalServerError) |
| 147 | + } |
| 148 | + } |
| 149 | +} |
| 150 | + |
| 151 | +// commas formats a non-negative integer string with comma separators. |
| 152 | +func commas(s string) string { |
| 153 | + for i := len(s); i > 3; { |
| 154 | + i -= 3 |
| 155 | + s = s[:i] + "," + s[i:] |
| 156 | + } |
| 157 | + return s |
| 158 | +} |
| 159 | + |
| 160 | +func fuint64(v uint64) string { |
| 161 | + return commas(strconv.FormatUint(v, 10)) |
| 162 | +} |
| 163 | + |
| 164 | +func fuint32(v uint32) string { |
| 165 | + return commas(strconv.FormatUint(uint64(v), 10)) |
| 166 | +} |
| 167 | + |
| 168 | +var baseTmpl = template.Must(template.New("").Funcs(template.FuncMap{ |
| 169 | + "fuint64": fuint64, |
| 170 | + "fuint32": fuint32, |
| 171 | +}).Parse(` |
| 172 | +<html> |
| 173 | +<head> |
| 174 | +<title>{{template "title" .}}</title> |
| 175 | +<style> |
| 176 | +body { |
| 177 | + font-family: sans-serif; |
| 178 | + font-size: 1rem; |
| 179 | + line-height: 1.6; |
| 180 | + margin: 0; |
| 181 | + padding: 0; |
| 182 | +} |
| 183 | +nav { |
| 184 | + background: #2d2d2d; |
| 185 | + padding: 0.5rem 1rem; |
| 186 | +} |
| 187 | +nav a { |
| 188 | + color: #fff; |
| 189 | + text-decoration: none; |
| 190 | + margin-right: 1.5rem; |
| 191 | + font-size: 0.9rem; |
| 192 | +} |
| 193 | +nav a:hover { |
| 194 | + text-decoration: underline; |
| 195 | +} |
| 196 | +.content { |
| 197 | + padding: 1rem 2rem; |
| 198 | +} |
| 199 | +table { |
| 200 | + border-collapse: collapse; |
| 201 | + margin: 0.5rem 0; |
| 202 | +} |
| 203 | +td, th { |
| 204 | + padding: 0.25rem 0.75rem; |
| 205 | + text-align: left; |
| 206 | + border-bottom: 1px solid #eee; |
| 207 | +} |
| 208 | +td.value { |
| 209 | + text-align: right; |
| 210 | + font-family: monospace; |
| 211 | +} |
| 212 | +th { |
| 213 | + border-bottom: 2px solid #ddd; |
| 214 | + font-weight: 600; |
| 215 | +} |
| 216 | +pre { |
| 217 | + background: #f5f5f5; |
| 218 | + padding: 1rem; |
| 219 | + overflow-x: auto; |
| 220 | + font-size: 0.85rem; |
| 221 | +} |
| 222 | +h1 { margin-top: 0; } |
| 223 | +.label { color: #555; } |
| 224 | +</style> |
| 225 | +{{block "head" .}}{{end}} |
| 226 | +</head> |
| 227 | +<body> |
| 228 | +<nav> |
| 229 | +<a href="/">Main</a> |
| 230 | +<a href="/info">Info</a> |
| 231 | +<a href="/memory">Memory</a> |
| 232 | +<a href="/debug/pprof">Profiling</a> |
| 233 | +</nav> |
| 234 | +<div class="content"> |
| 235 | +<h1>{{template "title" .}}</h1> |
| 236 | +{{block "body" .}} |
| 237 | +Unknown page |
| 238 | +{{end}} |
| 239 | +</div> |
| 240 | +</body> |
| 241 | +</html> |
| 242 | +`)) |
| 243 | + |
| 244 | +var mainTmpl = template.Must(template.Must(baseTmpl.Clone()).Parse(` |
| 245 | +{{define "title"}}Buf LSP Debug{{end}} |
| 246 | +{{define "body"}} |
| 247 | +<h2>Server</h2> |
| 248 | +<table> |
| 249 | +<tr><td class="label">Version</td><td>{{.Version}}</td></tr> |
| 250 | +<tr><td class="label">Go version</td><td>{{.GoVersion}}</td></tr> |
| 251 | +<tr><td class="label">Platform</td><td>{{.GOOS}}/{{.GOARCH}}</td></tr> |
| 252 | +<tr><td class="label">PID</td><td>{{.PID}}</td></tr> |
| 253 | +<tr><td class="label">Started</td><td>{{.StartTime.Format "2006-01-02 15:04:05"}}</td></tr> |
| 254 | +<tr><td class="label">Uptime</td><td>{{.Uptime}}</td></tr> |
| 255 | +</table> |
| 256 | +
|
| 257 | +<h2>Debug Pages</h2> |
| 258 | +<ul> |
| 259 | +<li><a href="/info">Server info and build details</a></li> |
| 260 | +<li><a href="/memory">Memory usage</a></li> |
| 261 | +<li><a href="/debug/pprof">Profiling (pprof)</a></li> |
| 262 | +</ul> |
| 263 | +
|
| 264 | +<h2>Profiles</h2> |
| 265 | +<ul> |
| 266 | +<li><a href="/debug/pprof/goroutine?debug=1">Goroutines</a></li> |
| 267 | +<li><a href="/debug/pprof/heap?debug=1">Heap</a></li> |
| 268 | +<li><a href="/debug/pprof/allocs?debug=1">Allocs</a></li> |
| 269 | +<li><a href="/debug/pprof/block?debug=1">Block</a></li> |
| 270 | +<li><a href="/debug/pprof/mutex?debug=1">Mutex</a></li> |
| 271 | +<li><a href="/debug/pprof/threadcreate?debug=1">Thread create</a></li> |
| 272 | +</ul> |
| 273 | +{{end}} |
| 274 | +`)) |
| 275 | + |
| 276 | +var infoTmpl = template.Must(template.Must(baseTmpl.Clone()).Parse(` |
| 277 | +{{define "title"}}Buf LSP Info{{end}} |
| 278 | +{{define "body"}} |
| 279 | +<h2>Server</h2> |
| 280 | +<table> |
| 281 | +<tr><td class="label">Version</td><td>{{.Version}}</td></tr> |
| 282 | +<tr><td class="label">Go version</td><td>{{.GoVersion}}</td></tr> |
| 283 | +<tr><td class="label">Platform</td><td>{{.GOOS}}/{{.GOARCH}}</td></tr> |
| 284 | +<tr><td class="label">PID</td><td>{{.PID}}</td></tr> |
| 285 | +<tr><td class="label">Started</td><td>{{.StartTime.Format "2006-01-02 15:04:05"}}</td></tr> |
| 286 | +<tr><td class="label">Uptime</td><td>{{.Uptime}}</td></tr> |
| 287 | +<tr><td class="label">NumCPU</td><td>{{.NumCPU}}</td></tr> |
| 288 | +<tr><td class="label">GOMAXPROCS</td><td>{{.GOMAXPROCS}}</td></tr> |
| 289 | +<tr><td class="label">Goroutines</td><td>{{.NumGoroutine}}</td></tr> |
| 290 | +</table> |
| 291 | +
|
| 292 | +{{if .BuildInfo}} |
| 293 | +<h2>Build Info</h2> |
| 294 | +<pre>{{.BuildInfo}}</pre> |
| 295 | +{{end}} |
| 296 | +{{end}} |
| 297 | +`)) |
| 298 | + |
| 299 | +var memoryTmpl = template.Must(template.Must(baseTmpl.Clone()).Parse(` |
| 300 | +{{define "title"}}Buf LSP Memory{{end}} |
| 301 | +{{define "body"}} |
| 302 | +<h2>Stats</h2> |
| 303 | +<table> |
| 304 | +<tr><td class="label">Allocated bytes</td><td class="value">{{fuint64 .HeapAlloc}}</td></tr> |
| 305 | +<tr><td class="label">Total allocated bytes</td><td class="value">{{fuint64 .TotalAlloc}}</td></tr> |
| 306 | +<tr><td class="label">System bytes</td><td class="value">{{fuint64 .Sys}}</td></tr> |
| 307 | +<tr><td class="label">Heap system bytes</td><td class="value">{{fuint64 .HeapSys}}</td></tr> |
| 308 | +<tr><td class="label">Malloc calls</td><td class="value">{{fuint64 .Mallocs}}</td></tr> |
| 309 | +<tr><td class="label">Frees</td><td class="value">{{fuint64 .Frees}}</td></tr> |
| 310 | +<tr><td class="label">Idle heap bytes</td><td class="value">{{fuint64 .HeapIdle}}</td></tr> |
| 311 | +<tr><td class="label">In use bytes</td><td class="value">{{fuint64 .HeapInuse}}</td></tr> |
| 312 | +<tr><td class="label">Released to system bytes</td><td class="value">{{fuint64 .HeapReleased}}</td></tr> |
| 313 | +<tr><td class="label">Heap object count</td><td class="value">{{fuint64 .HeapObjects}}</td></tr> |
| 314 | +<tr><td class="label">Stack in use bytes</td><td class="value">{{fuint64 .StackInuse}}</td></tr> |
| 315 | +<tr><td class="label">Stack from system bytes</td><td class="value">{{fuint64 .StackSys}}</td></tr> |
| 316 | +<tr><td class="label">Bucket hash bytes</td><td class="value">{{fuint64 .BuckHashSys}}</td></tr> |
| 317 | +<tr><td class="label">GC metadata bytes</td><td class="value">{{fuint64 .GCSys}}</td></tr> |
| 318 | +<tr><td class="label">Off heap bytes</td><td class="value">{{fuint64 .OtherSys}}</td></tr> |
| 319 | +</table> |
| 320 | +<h2>By Size</h2> |
| 321 | +<table> |
| 322 | +<tr><th>Size</th><th>Mallocs</th><th>Frees</th></tr> |
| 323 | +{{range .BySize}}<tr><td class="value">{{fuint32 .Size}}</td><td class="value">{{fuint64 .Mallocs}}</td><td class="value">{{fuint64 .Frees}}</td></tr>{{end}} |
| 324 | +</table> |
| 325 | +{{end}} |
| 326 | +`)) |
0 commit comments