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 }