github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/diagnostic/dump/default.go (about)

     1  // Copyright 2019 GRAIL, Inc. All rights reserved.
     2  // Use of this source code is governed by the Apache 2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  package dump
     6  
     7  import (
     8  	"archive/zip"
     9  	"context"
    10  	"encoding/json"
    11  	"expvar"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"path/filepath"
    16  	"runtime"
    17  	"runtime/pprof"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/Schaudge/grailbase/log"
    22  	"github.com/shirou/gopsutil/cpu"
    23  	"github.com/shirou/gopsutil/load"
    24  	"github.com/shirou/gopsutil/mem"
    25  )
    26  
    27  // DefaultRegistry is a default registry that has this process's GUID as its ID.
    28  var DefaultRegistry = NewRegistry(readExec())
    29  
    30  // Register registers a new part to be included in the dump of the
    31  // DefaultRegistry. name will become the filename of the part file in the dump
    32  // tarball. f will be called to produce the contents of that file.
    33  func Register(name string, f Func) {
    34  	DefaultRegistry.Register(name, f)
    35  }
    36  
    37  // WriteDump writes a dump of the default registry.
    38  func WriteDump(ctx context.Context, pfx string, zw *zip.Writer) {
    39  	DefaultRegistry.WriteDump(ctx, pfx, zw)
    40  }
    41  
    42  // Name returns the name of the default registry. See (*Registry).Name.
    43  func Name() string {
    44  	return DefaultRegistry.Name()
    45  }
    46  
    47  // readExec returns a sanitized version of the executable name, if it can be
    48  // determined. If not, returns "unknown".
    49  func readExec() string {
    50  	const unknown = "unknown"
    51  	execPath, err := os.Executable()
    52  	if err != nil {
    53  		return unknown
    54  	}
    55  	rawExec := filepath.Base(execPath)
    56  	var sanitized strings.Builder
    57  	for _, r := range rawExec {
    58  		if (r == '-' || 'a' <= r && r <= 'z') || ('0' <= r && r <= '9') {
    59  			sanitized.WriteRune(r)
    60  		}
    61  	}
    62  	if sanitized.Len() == 0 {
    63  		return unknown
    64  	}
    65  	return sanitized.String()
    66  }
    67  
    68  // shellQuote quotes a string to be used as an argument in an sh command line.
    69  func shellQuote(s string) string {
    70  	// We wrap with single quotes, as they will work with any string except
    71  	// those with single quotes. We handle single quotes by tranforming them
    72  	// into "'\''" and letting the shell concatenate the strings back together.
    73  	return "'" + strings.Replace(s, "'", `'\''`, -1) + "'"
    74  }
    75  
    76  // dumpCmdline writes the command-line of the current execution. It writes it
    77  // in a format that can be directly pasted into sh to be run.
    78  func dumpCmdline(ctx context.Context, w io.Writer) error {
    79  	args := make([]string, len(os.Args))
    80  	for i := range args {
    81  		args[i] = shellQuote(os.Args[i])
    82  	}
    83  	_, err := io.WriteString(w, strings.Join(args, " "))
    84  	return err
    85  }
    86  
    87  func dumpCpuinfo(ctx context.Context, w io.Writer) error {
    88  	info, err := cpu.InfoWithContext(ctx)
    89  	if err != nil {
    90  		return fmt.Errorf("error getting cpuinfo: %v", err)
    91  	}
    92  	s, err := json.MarshalIndent(info, "", "    ")
    93  	if err != nil {
    94  		return fmt.Errorf("error marshaling cpuinfo: %v", err)
    95  	}
    96  	_, err = w.Write(s)
    97  	return err
    98  }
    99  
   100  func dumpLoadinfo(ctx context.Context, w io.Writer) error {
   101  	type loadinfo struct {
   102  		Avg  *load.AvgStat  `json:"average"`
   103  		Misc *load.MiscStat `json:"miscellaneous"`
   104  	}
   105  	var info loadinfo
   106  	avg, err := load.AvgWithContext(ctx)
   107  	if err != nil {
   108  		return fmt.Errorf("error getting load averages: %v", err)
   109  	}
   110  	info.Avg = avg
   111  	misc, err := load.MiscWithContext(ctx)
   112  	if err != nil {
   113  		return fmt.Errorf("error getting miscellaneous load stats: %v", err)
   114  	}
   115  	info.Misc = misc
   116  	s, err := json.MarshalIndent(info, "", "    ")
   117  	if err != nil {
   118  		return fmt.Errorf("error marshaling loadinfo: %v", err)
   119  	}
   120  	_, err = w.Write(s)
   121  	return err
   122  }
   123  
   124  func dumpMeminfo(ctx context.Context, w io.Writer) error {
   125  	type meminfo struct {
   126  		Virtual *mem.VirtualMemoryStat `json:"virtualMemory"`
   127  		Runtime runtime.MemStats       `json:"goRuntime"`
   128  	}
   129  	var info meminfo
   130  	vmem, err := mem.VirtualMemoryWithContext(ctx)
   131  	if err != nil {
   132  		return fmt.Errorf("error getting virtual memory stats: %v", err)
   133  	}
   134  	info.Virtual = vmem
   135  	runtime.ReadMemStats(&info.Runtime)
   136  	s, err := json.MarshalIndent(info, "", "    ")
   137  	if err != nil {
   138  		return fmt.Errorf("error marshaling meminfo: %v", err)
   139  	}
   140  	_, err = w.Write(s)
   141  	if err != nil {
   142  		return fmt.Errorf("error writing memory stats: %v", err)
   143  	}
   144  	return nil
   145  }
   146  
   147  // dumpGoroutine writes current goroutines with human-readable source
   148  // locations.
   149  func dumpGoroutine(ctx context.Context, w io.Writer) error {
   150  	p := pprof.Lookup("goroutine")
   151  	if p == nil {
   152  		panic("no goroutine profile")
   153  	}
   154  	// debug == 2 prints goroutine stacks in the same form as that printed for
   155  	// an unrecovered panic.
   156  	return p.WriteTo(w, 2)
   157  }
   158  
   159  // dumpPprofHeap writes a pprof heap profile.
   160  func dumpPprofHeap(ctx context.Context, w io.Writer) error {
   161  	p := pprof.Lookup("heap")
   162  	if p == nil {
   163  		panic("no heap profile")
   164  	}
   165  	return p.WriteTo(w, 0)
   166  }
   167  
   168  // dumpPprofMutex writes a fraction of the stack traces of goroutines with
   169  // contended mutexes.
   170  func dumpPprofMutex(ctx context.Context, w io.Writer) error {
   171  	p := pprof.Lookup("mutex")
   172  	if p == nil {
   173  		panic("no mutex profile")
   174  	}
   175  	// debug == 1 makes use function names instead of hexadecimal addresses, so
   176  	// it can also be human-readable.
   177  	return p.WriteTo(w, 1)
   178  }
   179  
   180  // dumpPprofHeap writes a pprof CPU profile sampled for 30 seconds or until the
   181  // context is done, whichever is shorter.
   182  func dumpPprofProfile(ctx context.Context, w io.Writer) error {
   183  	if err := pprof.StartCPUProfile(w); err != nil {
   184  		return err
   185  	}
   186  	startTime := time.Now()
   187  	defer pprof.StopCPUProfile()
   188  	select {
   189  	case <-time.After(30 * time.Second):
   190  	case <-ctx.Done():
   191  		d := time.Since(startTime)
   192  		log.Debug.Printf("dump: CPU profile cut short to %s", d.String())
   193  	}
   194  	return nil
   195  }
   196  
   197  // dumpVars writes public variables exported by the expvar package. The output
   198  // is equivalent to the output of the "/debug/vars" endpoint.
   199  func dumpVars(ctx context.Context, w io.Writer) error {
   200  	if _, err := fmt.Fprintf(w, "{\n"); err != nil {
   201  		return err
   202  	}
   203  	var (
   204  		err   error
   205  		first = true
   206  	)
   207  	expvar.Do(func(kv expvar.KeyValue) {
   208  		if !first {
   209  			if _, err = fmt.Fprintf(w, ",\n"); err != nil {
   210  				return
   211  			}
   212  		}
   213  		first = false
   214  		if _, err = fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value); err != nil {
   215  			return
   216  		}
   217  	})
   218  	if err != nil {
   219  		return err
   220  	}
   221  	if _, err := fmt.Fprintf(w, "\n}\n"); err != nil {
   222  		return err
   223  	}
   224  	return nil
   225  }
   226  
   227  // Func is the type of a function that is registered in (*Registry).Register to