@@ -2,13 +2,15 @@ package cmd
22
33import (
44 "bytes"
5+ "context"
56 "encoding/json"
67 "errors"
78 "fmt"
89 "io"
910 "net/http"
1011 "os"
1112 "path/filepath"
13+ "slices"
1214 "strings"
1315 "sync"
1416 "time"
@@ -20,6 +22,7 @@ import (
2022
2123 "github.com/smartcontractkit/chainlink/v2/core/sessions"
2224 "github.com/smartcontractkit/chainlink/v2/core/utils"
25+ "github.com/smartcontractkit/chainlink/v2/core/web"
2326 "github.com/smartcontractkit/chainlink/v2/core/web/presenters"
2427)
2528
@@ -65,6 +68,10 @@ func initAdminSubCmds(s *Shell) []cli.Command {
6568 Usage : "output directory of the captured profile" ,
6669 Value : "/tmp/" ,
6770 },
71+ cli.StringSliceFlag {
72+ Name : "vitals, v" ,
73+ Usage : "vitals to collect, can be specified multiple times. Options: 'allocs', 'block', 'cmdline', 'goroutine', 'heap', 'mutex', 'profile', 'threadcreate', 'trace'" ,
74+ },
6875 },
6976 },
7077 {
@@ -319,16 +326,13 @@ func (s *Shell) Status(c *cli.Context) error {
319326// Profile will collect pprof metrics and store them in a folder.
320327func (s * Shell ) Profile (c * cli.Context ) error {
321328 ctx := s .ctx ()
322- seconds := c .Uint ("seconds" )
329+ seconds := c .Int ("seconds" )
323330 baseDir := c .String ("output_dir" )
324331
325332 genDir := filepath .Join (baseDir , "debuginfo-" + time .Now ().Format (time .RFC3339 ))
326333
327- if err := os .Mkdir (genDir , 0o755 ); err != nil {
328- return s .errorOut (err )
329- }
330- var wgPprof sync.WaitGroup
331- vitals := []string {
334+ vitals := c .StringSlice ("vitals" )
335+ allVitals := []string {
332336 "allocs" , // A sampling of all past memory allocations
333337 "block" , // Stack traces that led to blocking on synchronization primitives
334338 "cmdline" , // The command line invocation of the current program
@@ -339,15 +343,100 @@ func (s *Shell) Profile(c *cli.Context) error {
339343 "threadcreate" , // Stack traces that led to the creation of new OS threads
340344 "trace" , // A trace of execution of the current program.
341345 }
342- wgPprof .Add (len (vitals ))
343- s .Logger .Infof ("Collecting profiles: %v" , vitals )
346+ if len (vitals ) == 0 {
347+ vitals = slices .Clone (allVitals )
348+ } else if slices .ContainsFunc (vitals , func (s string ) bool { return ! slices .Contains (allVitals , s ) }) {
349+ return fmt .Errorf ("invalid vitals: must be from the set: %v" , allVitals )
350+ }
351+
352+ plugins , err := s .discoverPlugins (ctx )
353+ if err != nil {
354+ return s .errorOut (err )
355+ }
356+ var names []string
357+ for _ , group := range plugins {
358+ if name := group .Labels [web .LabelMetaPluginName ]; name != "" {
359+ names = append (names , name )
360+ }
361+ }
362+
363+ if len (names ) == 0 {
364+ s .Logger .Infof ("Collecting profiles: %v" , vitals )
365+ } else {
366+ s .Logger .Infof ("Collecting profiles from host and %d plugins: %v" , len (names ), vitals )
367+ }
344368 s .Logger .Infof ("writing debug info to %s" , genDir )
345369
370+ var wg sync.WaitGroup
371+ errs := make ([]error , len (names )+ 1 )
372+ wg .Add (len (names ) + 1 )
373+ go func () {
374+ defer wg .Done ()
375+ errs [0 ] = s .profile (ctx , genDir , "" , vitals , seconds )
376+ }()
377+ for i , name := range names {
378+ go func () {
379+ defer wg .Done ()
380+ errs [i ] = s .profile (ctx , genDir , name , vitals , seconds )
381+ }()
382+ }
383+ wg .Wait ()
384+
385+ err = errors .Join (errs ... )
386+ if err != nil {
387+ return s .errorOut (err )
388+ }
389+ return nil
390+ }
391+ func (s * Shell ) discoverPlugins (ctx context.Context ) (
392+ got []struct {
393+ Targets []string `yaml:"targets"`
394+ Labels map [string ]string `yaml:"labels"`
395+ },
396+ err error ,
397+ ) {
398+ resp , err := s .HTTP .Get (ctx , "/discovery" )
399+ if err != nil {
400+ return
401+ }
402+ defer func () {
403+ if resp .Body != nil {
404+ resp .Body .Close ()
405+ }
406+ }()
407+ data , err := io .ReadAll (resp .Body )
408+ if err != nil {
409+ return
410+ }
411+
412+ if err = json .Unmarshal (data , & got ); err != nil {
413+ s .Logger .Errorf ("failed to unmarshal discovery response: %s" , string (data ))
414+ return
415+ }
416+ return
417+ }
418+
419+ func (s * Shell ) profile (ctx context.Context , genDir string , name string , vitals []string , seconds int ) error {
420+ lggr := s .Logger
421+ path := "/v2"
422+ if name != "" {
423+ genDir = filepath .Join (genDir , "plugins" , name )
424+ path += "/plugins/" + name
425+ lggr = lggr .With ("plugin" , name )
426+ }
427+ if err := os .MkdirAll (genDir , 0o755 ); err != nil {
428+ return fmt .Errorf ("failed to create directory: %w" , err )
429+ }
430+
346431 errs := make (chan error , len (vitals ))
432+ var wgPprof sync.WaitGroup
433+ wgPprof .Add (len (vitals ))
347434 for _ , vt := range vitals {
348- go func (vt string ) {
435+ go func (ctx context. Context , vt string ) {
349436 defer wgPprof .Done ()
350- uri := fmt .Sprintf ("/v2/debug/pprof/%s?seconds=%d" , vt , seconds )
437+ ctx , cancel := context .WithTimeout (ctx , time .Duration (max (seconds , 0 )+ web .PPROFOverheadSeconds )* time .Second )
438+ defer cancel ()
439+ uri := fmt .Sprintf (path + "/debug/pprof/%s?seconds=%d" , vt , seconds )
351440 resp , err := s .HTTP .Get (ctx , uri )
352441 if err != nil {
353442 errs <- fmt .Errorf ("error collecting %s: %w" , vt , err )
@@ -403,12 +492,12 @@ func (s *Shell) Profile(c *cli.Context) error {
403492 errs <- fmt .Errorf ("error closing file for %s: %w" , vt , err )
404493 return
405494 }
406- }(vt )
495+ }(ctx , vt )
407496 }
408497 wgPprof .Wait ()
409498 close (errs )
410- // Atmost one err is emitted per vital.
411- s . Logger .Infof ("collected %d/%d profiles" , len (vitals )- len (errs ), len (vitals ))
499+ // At most one err is emitted per vital.
500+ lggr .Infof ("collected %d/%d profiles" , len (vitals )- len (errs ), len (vitals ))
412501 if len (errs ) > 0 {
413502 var merr error
414503 for err := range errs {
0 commit comments