github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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/charm.v6-unstable"
    15  	"gopkg.in/juju/names.v2"
    16  
    17  	"github.com/juju/juju/api"
    18  	actionapi "github.com/juju/juju/api/action"
    19  	"github.com/juju/juju/api/application"
    20  	"github.com/juju/juju/apiserver/params"
    21  	"github.com/juju/juju/cmd/juju/action"
    22  	"github.com/juju/juju/cmd/modelcmd"
    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 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  	service 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 &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.service = 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  type serviceClient interface {
   126  	GetCharmURL(service string) (*charm.URL, error)
   127  }
   128  
   129  var newServiceClient = func(root api.Connection) serviceClient {
   130  	return application.NewClient(root)
   131  }
   132  
   133  func isLocalCharmURL(conn api.Connection, entity string) (bool, error) {
   134  	serviceName := entity
   135  	var err error
   136  	if names.IsValidUnit(entity) {
   137  		serviceName, err = names.UnitApplication(entity)
   138  		if err != nil {
   139  			return false, errors.Trace(err)
   140  		}
   141  	}
   142  
   143  	client := newServiceClient(conn)
   144  	// TODO (mattyw, anastasiamac) The storage work might lead to an api
   145  	// allowing us to query charm url for a unit.
   146  	// When that api exists we should use that here.
   147  	url, err := client.GetCharmURL(serviceName)
   148  	if err != nil {
   149  		return false, errors.Trace(err)
   150  	}
   151  	return url.Schema == "local", nil
   152  }
   153  
   154  var newAPIConn = func(cmd modelcmd.ModelCommandBase) (api.Connection, error) {
   155  	return cmd.NewAPIRoot()
   156  }
   157  
   158  // Run implements Command.Run.
   159  func (c *collectMetricsCommand) Run(ctx *cmd.Context) error {
   160  	root, err := newAPIConn(c.ModelCommandBase)
   161  	if err != nil {
   162  		return errors.Trace(err)
   163  	}
   164  	runnerClient := newRunClient(root)
   165  	defer runnerClient.Close()
   166  
   167  	islocal, err := isLocalCharmURL(root, c.entity)
   168  	if err != nil {
   169  		return errors.Annotate(err, "failed to find charmURL for entity")
   170  	}
   171  	if !islocal {
   172  		return errors.Errorf("%q is not a local charm", c.entity)
   173  	}
   174  
   175  	units := []string{}
   176  	services := []string{}
   177  	if c.unit != "" {
   178  		units = []string{c.unit}
   179  	}
   180  	if c.service != "" {
   181  		services = []string{c.service}
   182  	}
   183  	runParams := params.RunParams{
   184  		Timeout:      commandTimeout,
   185  		Units:        units,
   186  		Applications: services,
   187  		Commands:     "nc -U ../metrics-collect.socket",
   188  	}
   189  
   190  	// trigger metrics collection
   191  	runResults, err := runnerClient.Run(runParams)
   192  	if err != nil {
   193  		return errors.Trace(err)
   194  	}
   195  
   196  	// We want to wait for the action results indefinitely.  Discard the tick.
   197  	wait := time.NewTimer(0 * time.Second)
   198  	_ = <-wait.C
   199  	// trigger sending metrics in parallel
   200  	resultChannel := make(chan string, len(runResults))
   201  	for _, result := range runResults {
   202  		r := result
   203  		if r.Error != nil {
   204  			fmt.Fprintf(ctx.Stdout, "failed to collect metrics: %v\n", err)
   205  			resultChannel <- "invalid id"
   206  			continue
   207  		}
   208  		tag, err := names.ParseActionTag(r.Action.Tag)
   209  		if err != nil {
   210  			fmt.Fprintf(ctx.Stdout, "failed to collect metrics: %v\n", err)
   211  			resultChannel <- "invalid id"
   212  			continue
   213  		}
   214  		actionResult, err := getActionResult(runnerClient, tag.Id(), wait)
   215  		if err != nil {
   216  			fmt.Fprintf(ctx.Stdout, "failed to collect metrics: %v\n", err)
   217  			resultChannel <- "invalid id"
   218  			continue
   219  		}
   220  		unitId, err := parseActionResult(actionResult)
   221  		if err != nil {
   222  			fmt.Fprintf(ctx.Stdout, "failed to collect metrics: %v\n", err)
   223  			resultChannel <- "invalid id"
   224  			continue
   225  		}
   226  		go func() {
   227  			defer func() {
   228  				resultChannel <- unitId
   229  			}()
   230  			sendParams := params.RunParams{
   231  				Timeout:  commandTimeout,
   232  				Units:    []string{unitId},
   233  				Commands: "nc -U ../metrics-send.socket",
   234  			}
   235  			sendResults, err := runnerClient.Run(sendParams)
   236  			if err != nil {
   237  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, err)
   238  				return
   239  			}
   240  			if len(sendResults) != 1 {
   241  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v\n", unitId)
   242  				return
   243  			}
   244  			if sendResults[0].Error != nil {
   245  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, sendResults[0].Error)
   246  				return
   247  			}
   248  			tag, err := names.ParseActionTag(sendResults[0].Action.Tag)
   249  			if err != nil {
   250  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, err)
   251  				return
   252  			}
   253  			actionResult, err := getActionResult(runnerClient, tag.Id(), wait)
   254  			if err != nil {
   255  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, err)
   256  				return
   257  			}
   258  			stdout, stderr, err := parseRunOutput(actionResult)
   259  			if err != nil {
   260  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, err)
   261  				return
   262  			}
   263  			if stdout != "ok" {
   264  				fmt.Fprintf(ctx.Stdout, "failed to send metrics for unit %v: %v\n", unitId, errors.New(stderr))
   265  			}
   266  		}()
   267  	}
   268  
   269  	for _ = range runResults {
   270  		// The default is to wait forever for the command to finish.
   271  		select {
   272  		case <-resultChannel:
   273  		}
   274  	}
   275  	return nil
   276  }
   277  
   278  // getActionResult abstracts over the action CLI function that we use here to fetch results
   279  var getActionResult = func(c runClient, actionId string, wait *time.Timer) (params.ActionResult, error) {
   280  	return action.GetActionResult(c, actionId, wait)
   281  }