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  }