github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/metricsdebug/collectmetrics.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package metricsdebug
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/juju/cmd"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/loggo"
    14  	"gopkg.in/juju/names.v2"
    15  
    16  	"github.com/juju/juju/api"
    17  	actionapi "github.com/juju/juju/api/action"
    18  	"github.com/juju/juju/apiserver/params"
    19  	jujucmd "github.com/juju/juju/cmd"
    20  	"github.com/juju/juju/cmd/juju/action"
    21  	"github.com/juju/juju/cmd/modelcmd"
    22  	"github.com/juju/juju/worker/metrics/sender"
    23  )
    24  
    25  // TODO(bogdanteleaga): update this once querying for actions by name is implemented.
    26  const collectMetricsDoc = `
    27  Trigger metrics collection
    28  
    29  This command waits for the metric collection to finish before returning.
    30  You may abort this command and it will continue to run asynchronously.
    31  Results may be checked by 'juju show-action-status'.
    32  `
    33  
    34  const (
    35  	// commandTimeout represents the timeout for executing the command itself
    36  	commandTimeout = 3 * time.Second
    37  )
    38  
    39  var logger = loggo.GetLogger("juju.cmd.juju.collect-metrics")
    40  
    41  // collectMetricsCommand retrieves metrics stored in the juju controller.
    42  type collectMetricsCommand struct {
    43  	modelcmd.ModelCommandBase
    44  	unit        string
    45  	application string
    46  	entity      string
    47  }
    48  
    49  // NewCollectMetricsCommand creates a new collectMetricsCommand.
    50  func NewCollectMetricsCommand() cmd.Command {
    51  	return modelcmd.Wrap(&collectMetricsCommand{})
    52  }
    53  
    54  // Info implements Command.Info.
    55  func (c *collectMetricsCommand) Info() *cmd.Info {
    56  	return jujucmd.Info(&cmd.Info{
    57  		Name:    "collect-metrics",
    58  		Args:    "[application or unit]",
    59  		Purpose: "Collect metrics on the given unit/application.",
    60  		Doc:     collectMetricsDoc,
    61  	})
    62  }
    63  
    64  // Init reads and verifies the cli arguments for the collectMetricsCommand
    65  func (c *collectMetricsCommand) Init(args []string) error {
    66  	if len(args) == 0 {
    67  		return errors.New("you need to specify a unit or application.")
    68  	}
    69  	c.entity = args[0]
    70  	if names.IsValidUnit(c.entity) {
    71  		c.unit = c.entity
    72  	} else if names.IsValidApplication(args[0]) {
    73  		c.application = c.entity
    74  	} else {
    75  		return errors.Errorf("%q is not a valid unit or application", args[0])
    76  	}
    77  	if err := cmd.CheckEmpty(args[1:]); err != nil {
    78  		return errors.Errorf("unknown command line arguments: " + strings.Join(args, ","))
    79  	}
    80  	return nil
    81  }
    82  
    83  type runClient interface {
    84  	action.APIClient
    85  	Run(run params.RunParams) ([]params.ActionResult, error)
    86  }
    87  
    88  var newRunClient = func(conn api.Connection) runClient {
    89  	return actionapi.NewClient(conn)
    90  }
    91  
    92  func parseRunOutput(result params.ActionResult) (string, string, error) {
    93  	if result.Error != nil {
    94  		return "", "", result.Error
    95  	}
    96  	stdout, ok := result.Output["Stdout"].(string)
    97  	if !ok {
    98  		return "", "", errors.New("could not read stdout")
    99  	}
   100  	stderr, ok := result.Output["Stderr"].(string)
   101  	if !ok {
   102  		return "", "", errors.New("could not read stderr")
   103  	}
   104  	return strings.Trim(stdout, " \t\n"), strings.Trim(stderr, " \t\n"), nil
   105  }
   106  
   107  func parseActionResult(result params.ActionResult) (string, error) {
   108  	if result.Action != nil {
   109  		logger.Infof("ran action id %v", result.Action.Tag)
   110  	}
   111  	_, stderr, err := parseRunOutput(result)
   112  	if err != nil {
   113  		return "", errors.Trace(err)
   114  	}
   115  	tag, err := names.ParseUnitTag(result.Action.Receiver)
   116  	if err != nil {
   117  		return "", errors.Trace(err)
   118  	}
   119  	if strings.Contains(stderr, "nc: unix connect failed: No such file or directory") {
   120  		return "", errors.New("no collect application listening: does application support metric collection?")
   121  	}
   122  	return tag.Id(), nil
   123  }
   124  
   125  var newAPIConn = func(cmd modelcmd.ModelCommandBase) (api.Connection, error) {
   126  	return cmd.NewAPIRoot()
   127  }
   128  
   129  // Run implements Command.Run.
   130  func (c *collectMetricsCommand) Run(ctx *cmd.Context) error {
   131  	root, err := newAPIConn(c.ModelCommandBase)
   132  	if err != nil {
   133  		return errors.Trace(err)
   134  	}
   135  	runnerClient := newRunClient(root)
   136  	defer runnerClient.Close()
   137  
   138  	units := []string{}
   139  	applications := []string{}
   140  	if c.unit != "" {
   141  		units = []string{c.unit}
   142  	}
   143  	if c.application != "" {
   144  		applications = []string{c.application}
   145  	}
   146  	runParams := params.RunParams{
   147  		Timeout:      commandTimeout,
   148  		Units:        units,
   149  		Applications: applications,
   150  		Commands:     "nc -U ../metrics-collect.socket",
   151  	}
   152  
   153  	// trigger metrics collection
   154  	runResults, err := runnerClient.Run(runParams)
   155  	if err != nil {
   156  		return errors.Trace(err)
   157  	}
   158  
   159  	// We want to wait for the action results indefinitely.  Discard the tick.
   160  	wait := time.NewTimer(0 * time.Second)
   161  	_ = <-wait.C
   162  	// trigger sending metrics in parallel
   163  	resultChannel := make(chan string, len(runResults))
   164  	for _, result := range runResults {
   165  		r := result
   166  		if r.Error != nil {
   167  			fmt.Fprintf(ctx.Stdout, "failed to collect metrics: %v\n", err)
   168  			resultChannel <- "invalid id"
   169  			continue
   170  		}
   171  		tag, err := names.ParseActionTag(r.Action.Tag)
   172  		if err != nil {
   173  			fmt.Fprintf(ctx.Stdout, "failed to collect metrics: %v\n", err)
   174  			resultChannel <- "invalid id"
   175  			continue
   176  		}
   177  		actionResult, err := getActionResult(runnerClient, tag.Id(), wait)
   178  		if err != nil {
   179  			fmt.Fprintf(ctx.Stdout, "failed to collect metrics: %v\n", err)
   180  			resultChannel <- "invalid id"
   181  			continue
   182  		}
   183  		unitId, err := parseActionResult(actionResult)
   184  		if err != nil {
   185  			fmt.Fprintf(ctx.Stdout, "failed to collect metrics: %v\n", err)
   186  			resultChannel <- "invalid id"
   187  			continue
   188  		}
   189  		go func() {
   190  			defer func() {
   191  				resultChannel <- unitId
   192  			}()
   193  			sendParams := params.RunParams{
   194  				Timeout:  commandTimeout,
   195  				Units:    []string{unitId},
   196  				Commands: "nc -U ../" + sender.DefaultMetricsSendSocketName,
   197  			}
   198  			sendResults, err := runnerClient.Run(sendParams)
   199  			if err != nil {
   200  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, err)
   201  				return
   202  			}
   203  			if len(sendResults) != 1 {
   204  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v\n", unitId)
   205  				return
   206  			}
   207  			if sendResults[0].Error != nil {
   208  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, sendResults[0].Error)
   209  				return
   210  			}
   211  			tag, err := names.ParseActionTag(sendResults[0].Action.Tag)
   212  			if err != nil {
   213  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, err)
   214  				return
   215  			}
   216  			actionResult, err := getActionResult(runnerClient, tag.Id(), wait)
   217  			if err != nil {
   218  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, err)
   219  				return
   220  			}
   221  			stdout, stderr, err := parseRunOutput(actionResult)
   222  			if err != nil {
   223  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, err)
   224  				return
   225  			}
   226  			if stdout != "ok" {
   227  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, errors.New(stderr))
   228  			}
   229  		}()
   230  	}
   231  
   232  	for range runResults {
   233  		// The default is to wait forever for the command to finish.
   234  		select {
   235  		case <-resultChannel:
   236  		}
   237  	}
   238  	return nil
   239  }
   240  
   241  // getActionResult abstracts over the action CLI function that we use here to fetch results
   242  var getActionResult = func(c runClient, actionId string, wait *time.Timer) (params.ActionResult, error) {
   243  	return action.GetActionResult(c, actionId, wait)
   244  }