github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/diag/diag.go (about) 1 /* 2 Copyright 2018 Mirantis 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // The backoff code for temporary Accept() errors is based on gRPC 18 // code. Original copyright notice follows: 19 /* 20 * 21 * Copyright 2014, Google Inc. 22 * All rights reserved. 23 * 24 * Redistribution and use in source and binary forms, with or without 25 * modification, are permitted provided that the following conditions are 26 * met: 27 * 28 * * Redistributions of source code must retain the above copyright 29 * notice, this list of conditions and the following disclaimer. 30 * * Redistributions in binary form must reproduce the above 31 * copyright notice, this list of conditions and the following disclaimer 32 * in the documentation and/or other materials provided with the 33 * distribution. 34 * * Neither the name of Google Inc. nor the names of its 35 * contributors may be used to endorse or promote products derived from 36 * this software without specific prior written permission. 37 * 38 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 39 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 40 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 41 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 42 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 43 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 44 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 45 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 46 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 47 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 48 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 49 * 50 */ 51 52 package diag 53 54 import ( 55 "encoding/json" 56 "errors" 57 "fmt" 58 "io/ioutil" 59 "log" 60 "net" 61 "os" 62 "os/exec" 63 "path/filepath" 64 "runtime" 65 "strings" 66 "sync" 67 "syscall" 68 "time" 69 70 "github.com/golang/glog" 71 ) 72 73 const ( 74 toplevelDirName = "diagnostics" 75 ) 76 77 // Result denotes the result of a diagnostics run. 78 type Result struct { 79 // Name is the name of the item sans extension. 80 Name string `json:"name,omitempty"` 81 // Ext is the file extension to use. 82 Ext string `json:"ext,omitempty"` 83 // Data is the content returned by the Source. 84 Data string `json:"data,omitempty"` 85 // IsDir specifies whether this diagnostics result 86 // needs to be unpacked to a directory. 87 IsDir bool `json:"isdir"` 88 // Children denotes the child items to be placed into the 89 // subdirectory that should be made for this Result during 90 // unpacking. 91 Children map[string]Result `json:"children,omitempty"` 92 // Error contains an error message in case if the Source 93 // has failed to provide the information. 94 Error string `json:"error,omitempty"` 95 } 96 97 // FileName returns the file name for this Result. 98 func (dr Result) FileName() string { 99 if dr.Ext != "" { 100 return fmt.Sprintf("%s.%s", dr.Name, dr.Ext) 101 } 102 return dr.Name 103 } 104 105 // Unpack unpacks Result under the specified directory. 106 func (dr Result) Unpack(parentDir string) error { 107 switch { 108 case dr.Name == "": 109 return errors.New("Result name is not set") 110 case dr.Error != "": 111 glog.Warningf("Error recorded for the diag item %q: %v", dr.Name, dr.Error) 112 return nil 113 case !dr.IsDir && len(dr.Children) != 0: 114 return errors.New("Result can't contain both Data and Children") 115 case dr.IsDir: 116 dirPath := filepath.Join(parentDir, dr.FileName()) 117 if err := os.MkdirAll(dirPath, 0777); err != nil { 118 return err 119 } 120 for _, child := range dr.Children { 121 if err := child.Unpack(dirPath); err != nil { 122 return fmt.Errorf("couldn't unpack diag result at %q: %v", dirPath, err) 123 } 124 } 125 return nil 126 default: 127 targetPath := filepath.Join(parentDir, dr.FileName()) 128 if err := ioutil.WriteFile(targetPath, []byte(dr.Data), 0777); err != nil { 129 return fmt.Errorf("error writing %q: %v", targetPath, err) 130 } 131 return nil 132 } 133 } 134 135 // ToJSON encodes Result into JSON. 136 func (dr Result) ToJSON() []byte { 137 bs, err := json.Marshal(dr) 138 if err != nil { 139 log.Panicf("Error marshalling Result: %v", err) 140 } 141 return bs 142 } 143 144 // Source speicifies a diagnostics information source 145 type Source interface { 146 // DiagnosticInfo returns diagnostic information for the 147 // source. DiagnosticInfo() may skip setting Name in the 148 // Result, in which case it'll be set to the name used to 149 // register the source. 150 DiagnosticInfo() (Result, error) 151 } 152 153 // Set denotes a set of diagnostics sources. 154 type Set struct { 155 sync.Mutex 156 sources map[string]Source 157 } 158 159 // NewDiagSet creates a new Set. 160 func NewDiagSet() *Set { 161 return &Set{sources: make(map[string]Source)} 162 } 163 164 // RegisterDiagSource registers a diagnostics source. 165 func (ds *Set) RegisterDiagSource(name string, source Source) { 166 ds.Lock() 167 defer ds.Unlock() 168 ds.sources[name] = source 169 } 170 171 // RunDiagnostics collects the diagnostic information from all of the 172 // available sources. 173 func (ds *Set) RunDiagnostics() Result { 174 ds.Lock() 175 defer ds.Unlock() 176 r := Result{ 177 Name: toplevelDirName, 178 IsDir: true, 179 Children: make(map[string]Result), 180 } 181 for name, src := range ds.sources { 182 dr, err := src.DiagnosticInfo() 183 if dr.Name == "" { 184 dr.Name = name 185 } 186 if err != nil { 187 r.Children[name] = Result{ 188 Name: dr.Name, 189 Error: err.Error(), 190 } 191 } else { 192 r.Children[name] = dr 193 } 194 } 195 return r 196 } 197 198 // Server denotes a diagnostics server that listens on a unix domain 199 // socket and spews out a piece of JSON content on a socket 200 // connection. 201 type Server struct { 202 sync.Mutex 203 ds *Set 204 ln net.Listener 205 doneCh chan struct{} 206 } 207 208 // NewServer makes a new diagnostics server using the specified Set. 209 // If diagSet is nil, DefaultDiagSet is used. 210 func NewServer(diagSet *Set) *Server { 211 if diagSet == nil { 212 diagSet = DefaultDiagSet 213 } 214 return &Server{ds: diagSet} 215 } 216 217 func (s *Server) dump(conn net.Conn) error { 218 defer conn.Close() 219 r := s.ds.RunDiagnostics() 220 bs, err := json.Marshal(&r) 221 if err != nil { 222 return fmt.Errorf("error marshalling diagnostics info: %v", err) 223 } 224 switch n, err := conn.Write(bs); { 225 case err != nil: 226 return err 227 case n < len(bs): 228 return errors.New("short write") 229 } 230 return nil 231 } 232 233 // Serve makes the server listen on the specified socket path. If 234 // readyCh is not nil, it'll be closed when the server is ready to 235 // accept connections. This function doesn't return till the server 236 // stops listening. 237 func (s *Server) Serve(socketPath string, readyCh chan struct{}) error { 238 err := syscall.Unlink(socketPath) 239 if err != nil && !os.IsNotExist(err) { 240 return err 241 } 242 s.Lock() 243 s.doneCh = make(chan struct{}) 244 defer close(s.doneCh) 245 s.ln, err = net.Listen("unix", socketPath) 246 s.Unlock() 247 if err != nil { 248 return err 249 } 250 defer s.ln.Close() 251 if readyCh != nil { 252 close(readyCh) 253 } 254 for { 255 var tempDelay time.Duration // how long to sleep on accept failure 256 257 for { 258 conn, err := s.ln.Accept() 259 if err != nil { 260 if ne, ok := err.(interface { 261 Temporary() bool 262 }); !ok || !ne.Temporary() { 263 glog.V(1).Infof("done serving; Accept = %v", err) 264 return err 265 } 266 if tempDelay == 0 { 267 tempDelay = 5 * time.Millisecond 268 } else { 269 tempDelay *= 2 270 } 271 if max := 1 * time.Second; tempDelay > max { 272 tempDelay = max 273 } 274 glog.Warningf("Accept error: %v; retrying in %v", err, tempDelay) 275 <-time.After(tempDelay) 276 continue 277 } 278 tempDelay = 0 279 280 if err := s.dump(conn); err != nil { 281 glog.Warningf("Error dumping diagnostics info: %v", err) 282 } 283 } 284 } 285 } 286 287 // Stop stops the server. 288 func (s *Server) Stop() { 289 s.Lock() 290 if s.ln != nil { 291 s.ln.Close() 292 s.Unlock() 293 <-s.doneCh 294 s.doneCh = nil 295 } else { 296 s.Unlock() 297 } 298 } 299 300 // RetrieveDiagnostics retrieves the diagnostic info from the 301 // specified UNIX domain socket. 302 func RetrieveDiagnostics(socketPath string) (Result, error) { 303 addr, err := net.ResolveUnixAddr("unix", socketPath) 304 if err != nil { 305 return Result{}, fmt.Errorf("failed to resolve unix addr %q: %v", socketPath, err) 306 } 307 308 conn, err := net.DialUnix("unix", nil, addr) 309 if err != nil { 310 return Result{}, fmt.Errorf("can't connect to %q: %v", socketPath, err) 311 } 312 313 bs, err := ioutil.ReadAll(conn) 314 if err != nil { 315 return Result{}, fmt.Errorf("can't read diagnostics: %v", err) 316 } 317 318 return DecodeDiagnostics(bs) 319 } 320 321 // DecodeDiagnostics loads the diagnostics info from the JSON data. 322 func DecodeDiagnostics(data []byte) (Result, error) { 323 var r Result 324 if err := json.Unmarshal(data, &r); err != nil { 325 return Result{}, fmt.Errorf("error unmarshalling the diagnostics: %v", err) 326 } 327 return r, nil 328 } 329 330 // CommandSource executes the specified command and returns the stdout 331 // contents as diagnostics info 332 type CommandSource struct { 333 ext string 334 cmd []string 335 } 336 337 var _ Source = &CommandSource{} 338 339 // NewCommandSource creates a new CommandSource. 340 func NewCommandSource(ext string, cmd []string) *CommandSource { 341 return &CommandSource{ 342 ext: ext, 343 cmd: cmd, 344 } 345 } 346 347 // DiagnosticInfo implements DiagnosticInfo method of the Source 348 // interface. 349 func (s *CommandSource) DiagnosticInfo() (Result, error) { 350 if len(s.cmd) == 0 { 351 return Result{}, errors.New("empty command") 352 } 353 r := Result{ 354 Ext: s.ext, 355 } 356 out, err := exec.Command(s.cmd[0], s.cmd[1:]...).Output() 357 if err == nil { 358 r.Data = string(out) 359 } else { 360 cmdStr := strings.Join(s.cmd, " ") 361 if ee, ok := err.(*exec.ExitError); ok { 362 return Result{}, fmt.Errorf("error running command %q: stderr:\n%s", cmdStr, ee.Stderr) 363 } 364 return Result{}, fmt.Errorf("error running command %q: %v", cmdStr, err) 365 } 366 return r, nil 367 } 368 369 // SimpleTextSourceFunc denotes a function that's invoked by 370 // SimpleTextSource to gather diagnostics info. 371 type SimpleTextSourceFunc func() (string, error) 372 373 // SimpleTextSource invokes the specified function that returns a 374 // string (and an error, if any) and wraps its result in Result 375 type SimpleTextSource struct { 376 ext string 377 toCall SimpleTextSourceFunc 378 } 379 380 var _ Source = &SimpleTextSource{} 381 382 // NewSimpleTextSource creates a new SimpleTextSource. 383 func NewSimpleTextSource(ext string, toCall SimpleTextSourceFunc) *SimpleTextSource { 384 return &SimpleTextSource{ 385 ext: ext, 386 toCall: toCall, 387 } 388 } 389 390 // DiagnosticInfo implements DiagnosticInfo method of the Source 391 // interface. 392 func (s *SimpleTextSource) DiagnosticInfo() (Result, error) { 393 out, err := s.toCall() 394 if err != nil { 395 return Result{}, err 396 } 397 return Result{ 398 Ext: s.ext, 399 Data: out, 400 }, nil 401 } 402 403 // LogDirSource bundles together log files from the specified directory. 404 type LogDirSource struct { 405 logDir string 406 } 407 408 // NewLogDirSource creates a new LogDirSource. 409 func NewLogDirSource(logDir string) *LogDirSource { 410 return &LogDirSource{ 411 logDir: logDir, 412 } 413 } 414 415 var _ Source = &LogDirSource{} 416 417 // DiagnosticInfo implements DiagnosticInfo method of the Source 418 // interface. 419 func (s *LogDirSource) DiagnosticInfo() (Result, error) { 420 files, err := ioutil.ReadDir(s.logDir) 421 if err != nil { 422 return Result{}, err 423 } 424 r := Result{ 425 IsDir: true, 426 Children: make(map[string]Result), 427 } 428 for _, fi := range files { 429 if fi.IsDir() { 430 continue 431 } 432 name := fi.Name() 433 if strings.HasPrefix(name, ".") { 434 continue 435 } 436 ext := filepath.Ext(name) 437 cur := Result{ 438 Name: name, 439 } 440 if ext != "" { 441 cur.Ext = ext[1:] 442 cur.Name = name[:len(name)-len(ext)] 443 } 444 fullPath := filepath.Join(s.logDir, name) 445 data, err := ioutil.ReadFile(fullPath) 446 if err != nil { 447 return Result{}, fmt.Errorf("error reading %q: %v", fullPath, err) 448 } 449 cur.Data = string(data) 450 r.Children[cur.Name] = cur 451 } 452 return r, nil 453 } 454 455 type stackDumpSource struct{} 456 457 func (s stackDumpSource) DiagnosticInfo() (Result, error) { 458 var buf []byte 459 var stackSize int 460 bufSize := 32768 461 for { 462 buf = make([]byte, bufSize) 463 stackSize = runtime.Stack(buf, true) 464 if stackSize < len(buf) { 465 break 466 } 467 bufSize *= 2 468 } 469 return Result{ 470 Ext: "log", 471 Data: string(buf[:stackSize]), 472 }, nil 473 } 474 475 // StackDumpSource dumps Go runtime stack. 476 var StackDumpSource Source = stackDumpSource{} 477 478 // DefaultDiagSet is the default Set to use. 479 var DefaultDiagSet = NewDiagSet() 480 481 func init() { 482 DefaultDiagSet.RegisterDiagSource("stack", StackDumpSource) 483 }