├── .gitignore ├── example ├── main.go └── Dockerfile ├── README.md ├── LICENSE ├── sigprof_test.go └── sigprof.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "github.com/tam7t/sigprof" 5 | 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | messages := make(chan string) 12 | 13 | // consumer 14 | go func() { 15 | for { 16 | select { 17 | case m := <-messages: 18 | fmt.Println(m) 19 | } 20 | } 21 | }() 22 | 23 | // producer 24 | go func() { 25 | for { 26 | time.Sleep(1 * time.Second) 27 | messages <- `ping` 28 | } 29 | }() 30 | 31 | // block indefinately 32 | select {} 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sigprof 2 | 3 | [![Join the chat at https://gitter.im/tam7t/sigprof](https://badges.gitter.im/tam7t/sigprof.svg)](https://gitter.im/tam7t/sigprof?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | Golang package for inspecting running processes. Similar to [net/http/pprof](https://golang.org/pkg/net/http/pprof/) but using `USR1` and `USR2` signals instead of HTTP server routes. 5 | 6 | # Usage 7 | Link the package: 8 | 9 | ```go 10 | import _ "github.com/tam7t/sigprof" 11 | ``` 12 | 13 | Send the `USR1` or `USR2` signal to inspect the process. 14 | 15 | ```bash 16 | kill -usr1 17 | ``` 18 | 19 | The default `USR1` profile is [goroutine](https://golang.org/pkg/runtime/pprof/#Profile). By default, `sigprof` will save results to timestamped files. 20 | 21 | ```bash 22 | go tool pprof profile-.prof 23 | ``` 24 | 25 | # Configuration 26 | 27 | `sigprof` loads its configuration from the following environment variables. 28 | 29 | * `USR1_PROF` - Profile executed on the `USR1` signal. Default: `goroutine` 30 | * `USR2_PROF` - Profile executed on the `USR2` signal. Default: `heap` 31 | * `SIG_PROF_OUT` - Specify the output location, either `file`, `stderr`, or 32 | `stdout`. Default: `file`. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Tommy Murphy 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of sigprof nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /sigprof_test.go: -------------------------------------------------------------------------------- 1 | package sigprof 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "syscall" 11 | "testing" 12 | ) 13 | 14 | type bufferCloser struct { 15 | *bytes.Buffer 16 | } 17 | 18 | func (bufferCloser) Close() error { return nil } 19 | 20 | type testProfiler struct{} 21 | 22 | func (testProfiler) writeProfile(w io.Writer, profileName string) error { 23 | fmt.Fprintf(w, "test %s\n", profileName) 24 | return nil 25 | } 26 | 27 | func TestStubs(t *testing.T) { 28 | outputs := map[string]*bytes.Buffer{} 29 | s := sigprof{ 30 | usr1: []string{"foo", "bar"}, 31 | usr2: []string{"baz", "quux"}, 32 | output: "orange", 33 | sigChanFactory: func() <-chan (os.Signal) { 34 | c := make(chan os.Signal) 35 | go func() { 36 | c <- syscall.SIGUSR1 37 | c <- syscall.SIGUSR2 38 | c <- syscall.SIGHUP 39 | close(c) 40 | }() 41 | return c 42 | }, 43 | writerFactory: func(profile string, out outputType) io.WriteCloser { 44 | if out != "orange" { 45 | t.Fatalf("unexpected output %q", out) 46 | } 47 | var buf bytes.Buffer 48 | outputs[profile] = &buf 49 | return bufferCloser{&buf} 50 | }, 51 | profilerFactory: func() profiler { 52 | return testProfiler{} 53 | }, 54 | } 55 | 56 | s.loop() 57 | 58 | if len(outputs) != 4 { 59 | t.Errorf("unexpected outputs len=%d", len(outputs)) 60 | } 61 | for _, profile := range []string{"foo", "bar", "baz", "quux"} { 62 | buf, ok := outputs[profile] 63 | if !ok { 64 | t.Errorf("missing expected profile %q", profile) 65 | } 66 | if buf.String() != "test "+profile+"\n" { 67 | t.Errorf("unexpected profiler contents: %q", buf.String()) 68 | } 69 | } 70 | } 71 | 72 | func TestPprof(t *testing.T) { 73 | outputs := []*bytes.Buffer{} 74 | s := sigprof{ 75 | usr1: []string{"goroutine"}, 76 | usr2: []string{"heap"}, 77 | output: "file", 78 | writerFactory: func(profile string, out outputType) io.WriteCloser { 79 | var buf bytes.Buffer 80 | outputs = append(outputs, &buf) 81 | return bufferCloser{&buf} 82 | }, 83 | sigChanFactory: func() <-chan os.Signal { 84 | ch := make(chan os.Signal) 85 | go func() { 86 | for i := 0; i < 100; i++ { 87 | ch <- syscall.SIGUSR1 88 | ch <- syscall.SIGUSR2 89 | } 90 | close(ch) 91 | }() 92 | return ch 93 | }, 94 | profilerFactory: newProfiler, 95 | } 96 | 97 | s.loop() 98 | 99 | if len(outputs) != 200 { 100 | t.Errorf("unexpected number of profiles: %d", len(outputs)) 101 | } 102 | 103 | var nHeap, nGoroutine int 104 | for _, output := range outputs { 105 | if strings.Contains(output.String(), "goroutine profile") { 106 | nGoroutine++ 107 | } else if strings.Contains(output.String(), "heap profile") { 108 | nHeap++ 109 | } 110 | } 111 | if nGoroutine != 100 { 112 | t.Errorf("unexpected goroutine profile count: %d", nGoroutine) 113 | } 114 | if nHeap != 100 { 115 | t.Errorf("unexpected heap profile count: %d", nHeap) 116 | } 117 | } 118 | 119 | func TestWriter(t *testing.T) { 120 | stdout := newWriter("blips", "stdout") 121 | if _, ok := stdout.(stdoutWriter); !ok { 122 | t.Errorf("stdout: got a %T instead", stdout) 123 | } 124 | stderr := newWriter("blops", "stderr") 125 | if _, ok := stderr.(stderrWriter); !ok { 126 | t.Errorf("stderr: got a %T instead", stderr) 127 | } 128 | whatever := newWriter("blups", "whatever") 129 | if _, ok := whatever.(stderrWriter); !ok { 130 | t.Errorf("default: got a %T instead", whatever) 131 | } 132 | file := newWriter("nitpicks", "file") 133 | if f, ok := file.(*os.File); !ok { 134 | t.Errorf("file: got a %T instead", file) 135 | } else { 136 | defer os.Remove(f.Name()) 137 | defer file.Close() 138 | if !strings.Contains(filepath.Base(f.Name()), "nitpicks.prof.") { 139 | t.Errorf("file: unexpected file name %q", f.Name()) 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /example/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM google/golang:1.3 2 | 3 | # get dependency 4 | RUN go get github.com/tam7t/sigprof 5 | 6 | COPY . ${GOPATH}/src/github.com/tam7t/sigprof/example 7 | WORKDIR ${GOPATH}/src/github.com/tam7t/sigprof/example 8 | 9 | # run binary 10 | RUN go run main.go 11 | 12 | # Build & run the docker container 13 | # ~ $ docker build . 14 | # 15 | # Show running container 16 | # ~ $ docker ps 17 | # CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 18 | # 046bb6bd1e45 ae36a05487424e97637e18f1c29c3fb0370b0fd1a0b988616cfa91af10af280e:latest "/bin/sh -c 'go run 11 seconds ago Up 10 seconds stoic_bartik 19 | # 20 | # Enter docker container shell 21 | # ~ $ docker exec -it 046bb6bd1e45 /bin/bash 22 | # root@ece227541990:/gopath/src/github.com/tam7t/sigprof/example# 23 | # 24 | # Find the pid of the process by iterating over /proc/ (usually pid 1) 25 | # root@ece227541990:/gopath/src/github.com/tam7t/sigprof/example# echo "$(cat /proc/1/cmdline)" 26 | # /bin/sh-cgo run main.go 27 | # 28 | # Here pid 14 is the one we want since we used go run instead of directly running executable. 29 | # root@ece227541990:/gopath/src/github.com/tam7t/sigprof/example# echo "$(cat /proc/14/cmdline)" 30 | # /tmp/go-build163951639/command-line-arguments/_obj/exe/main 31 | # 32 | # Lets send the signal 33 | # root@ece227541990:/gopath/src/github.com/tam7t/sigprof/example# ls 34 | # Dockerfile main main.go 35 | # root@ece227541990:/gopath/src/github.com/tam7t/sigprof/example# kill -usr1 14 36 | # root@ece227541990:/gopath/src/github.com/tam7t/sigprof/example# ls 37 | # Dockerfile main main.go profile-2015-05-12 14:43:08.72195103 +0000 UTC.prof 38 | # 39 | # And examine the file 40 | # root@ece227541990:/gopath/src/github.com/tam7t/sigprof/example# cat "profile-2015-05-12 14:43:08.72195103 +0000 UTC.prof" 41 | # goroutine profile: total 9 42 | # 1 @ 0x45f26c 0x45f01b 0x45bfc3 0x42ae64 0x42abb1 0x413460 43 | # # 0x45f26c runtime/pprof.writeRuntimeProfile+0xcc /usr/local/go/src/pkg/runtime/pprof/pprof.go:540 44 | # # 0x45f01b runtime/pprof.writeGoroutine+0x9b /usr/local/go/src/pkg/runtime/pprof/pprof.go:502 45 | # # 0x45bfc3 runtime/pprof.(*Profile).WriteTo+0xd3 /usr/local/go/src/pkg/runtime/pprof/pprof.go:229 46 | # # 0x42ae64 github.com/tam7t/sigprof.lookup+0x144 /gopath/src/github.com/tam7t/sigprof/sigprof.go:106 47 | # # 0x42abb1 github.com/tam7t/sigprof.profile+0x211 /gopath/src/github.com/tam7t/sigprof/sigprof.go:92 48 | # 49 | # 1 @ 0x4131c9 0x41cf59 0x400cb3 0x410c9a 0x413460 50 | # # 0x400cb3 main.main+0xb3 /gopath/src/github.com/tam7t/sigprof/example/main.go:32 51 | # # 0x410c9a runtime.main+0x11a /usr/local/go/src/pkg/runtime/proc.c:247 52 | # 53 | # 1 @ 0x4047f6 0x40c943 0x413460 54 | # # 0x4047f6 runtime.notetsleepg+0x46 /usr/local/go/src/pkg/runtime/lock_futex.c:198 55 | # # 0x40c943 runtime.MHeap_Scavenger+0xa3 /usr/local/go/src/pkg/runtime/mheap.c:532 56 | # 57 | # 1 @ 0x4131c9 0x41324b 0x4095ea 0x413460 58 | # # 0x4095ea bgsweep+0x9a /usr/local/go/src/pkg/runtime/mgc0.c:1993 59 | # 60 | # 1 @ 0x4131c9 0x41324b 0x40a90f 0x413460 61 | # # 0x40a90f runfinq+0xcf /usr/local/go/src/pkg/runtime/mgc0.c:2644 62 | # 63 | # 1 @ 0x4047f6 0x424e2a 0x45b3be 0x413460 64 | # # 0x45b3be os/signal.loop+0x1e /usr/local/go/src/pkg/os/signal/signal_unix.go:21 65 | 66 | # 1 @ 0x4131c9 0x41324b 0x41cc98 0x41cdc8 0x400d2d 0x413460 67 | # # 0x41cc98 chanrecv+0x4e8 /usr/local/go/src/pkg/runtime/chan.goc:268 68 | # # 0x41cdc8 runtime.chanrecv1+0x38 /usr/local/go/src/pkg/runtime/chan.goc:352 69 | # # 0x400d2d main.func·001+0x6d /gopath/src/github.com/tam7t/sigprof/example/main.go:17 70 | # 71 | # 1 @ 0x4131c9 0x41324b 0x42706a 0x426f61 0x400e0f 0x413460 72 | # # 0x426f61 time.Sleep+0x31 /usr/local/go/src/pkg/runtime/time.goc:39 73 | # # 0x400e0f main.func·002+0x2f /gopath/src/github.com/tam7t/sigprof/example/main.go:26 74 | # 75 | # 1 @ 0x4047f6 0x42730a 0x413460 76 | # # 0x42730a timerproc+0xda /usr/local/go/src/pkg/runtime/time.goc:260 77 | 78 | -------------------------------------------------------------------------------- /sigprof.go: -------------------------------------------------------------------------------- 1 | // Package sigprof provides signal-triggered profiling. 2 | package sigprof 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "os/signal" 11 | "path/filepath" 12 | "runtime/pprof" 13 | "strings" 14 | "syscall" 15 | ) 16 | 17 | func init() { 18 | s := newSigprof() 19 | go s.loop() 20 | } 21 | 22 | type stderrWriter struct{} 23 | 24 | // Write implements io.Writer. 25 | func (w stderrWriter) Write(p []byte) (int, error) { 26 | return os.Stderr.Write(p) 27 | } 28 | 29 | // Close implements io.Closer. 30 | func (w stderrWriter) Close() error { 31 | return nil 32 | } 33 | 34 | type stdoutWriter struct{} 35 | 36 | // Write implements io.Writer. 37 | func (w stdoutWriter) Write(p []byte) (int, error) { 38 | return os.Stdout.Write(p) 39 | } 40 | 41 | // Close implements io.Closer. 42 | func (w stdoutWriter) Close() error { 43 | return nil 44 | } 45 | 46 | type outputType string 47 | 48 | const ( 49 | stdoutOutput = outputType("stdout") 50 | stderrOutput = outputType("stderr") 51 | fileOutput = outputType("file") 52 | ) 53 | 54 | type sigprof struct { 55 | usr1, usr2 []string 56 | output outputType 57 | 58 | writerFactory func(profile string, output outputType) io.WriteCloser 59 | profilerFactory func() profiler 60 | sigChanFactory func() <-chan (os.Signal) 61 | } 62 | 63 | func newSigprof() sigprof { 64 | s := sigprof{ 65 | writerFactory: newWriter, 66 | profilerFactory: newProfiler, 67 | sigChanFactory: newSigChan, 68 | } 69 | 70 | usr1EnvStr := os.Getenv(`SIGPROF_USR1`) 71 | if usr1EnvStr == "" { 72 | usr1EnvStr = "goroutine" 73 | } 74 | s.usr1 = strings.Split(usr1EnvStr, ",") 75 | 76 | usr2EnvStr := os.Getenv(`SIGPROF_USR2`) 77 | if usr2EnvStr == "" { 78 | usr2EnvStr = "heap" 79 | } 80 | s.usr2 = strings.Split(usr2EnvStr, ",") 81 | 82 | output := os.Getenv(`SIGPROF_OUT`) 83 | if output == "" { 84 | output = "file" 85 | } 86 | s.output = outputType(output) 87 | 88 | return s 89 | } 90 | 91 | // loop handles signals and writes profiles. 92 | func (s *sigprof) loop() { 93 | c := s.sigChanFactory() 94 | for { 95 | select { 96 | case sig, ok := <-c: 97 | if !ok { 98 | return 99 | } 100 | s.profileSignal(sig) 101 | } 102 | } 103 | } 104 | 105 | func newSigChan() <-chan (os.Signal) { 106 | c := make(chan os.Signal) 107 | signal.Notify(c, syscall.SIGUSR1, syscall.SIGUSR2) 108 | return c 109 | } 110 | 111 | // profileSignal writes the profiles for the given signal. 112 | func (s *sigprof) profileSignal(sig os.Signal) { 113 | var profiles []string 114 | switch sig { 115 | case syscall.SIGUSR1: 116 | profiles = s.usr1 117 | case syscall.SIGUSR2: 118 | profiles = s.usr2 119 | default: 120 | return 121 | } 122 | 123 | for _, profile := range profiles { 124 | w := s.writer(profile) 125 | s.profile(profile, w) 126 | } 127 | } 128 | 129 | // writer returns an io.WriteCloser to where the profile should be written. 130 | func (s *sigprof) writer(profile string) io.WriteCloser { 131 | return s.writerFactory(profile, s.output) 132 | } 133 | 134 | func newWriter(profile string, output outputType) io.WriteCloser { 135 | switch output { 136 | case "file": 137 | f, err := ioutil.TempFile("", fmt.Sprintf("%s.%s.prof.", filepath.Base(os.Args[0]), profile)) 138 | if err != nil { 139 | log.Printf("failed to create file for %s profile: %v", profile, err) 140 | return stderrWriter{} 141 | } 142 | log.Printf("writing %s profile to %s", profile, f.Name()) 143 | return f 144 | case "stdout": 145 | return stdoutWriter{} 146 | case "stderr": 147 | return stderrWriter{} 148 | default: 149 | return stderrWriter{} 150 | } 151 | } 152 | 153 | type profiler interface { 154 | writeProfile(w io.Writer, profileName string) error 155 | } 156 | 157 | type pprofiler struct{} 158 | 159 | func (pprofiler) writeProfile(w io.Writer, profileName string) error { 160 | p := pprof.Lookup(profileName) 161 | if p == nil { 162 | return fmt.Errorf("failed to lookup profile %q", profileName) 163 | } 164 | return p.WriteTo(w, 1) 165 | } 166 | 167 | func newProfiler() profiler { 168 | return pprofiler{} 169 | } 170 | 171 | func (s *sigprof) profile(profileName string, w io.WriteCloser) { 172 | defer w.Close() 173 | p := s.profilerFactory() 174 | err := p.writeProfile(w, profileName) 175 | if err != nil { 176 | log.Printf("failed to write %s profile: %v", profileName, err) 177 | } 178 | } 179 | --------------------------------------------------------------------------------