OLD | NEW |
1 // Copyright 2013 Canonical Ltd. | 1 // Copyright 2013, 2014 Canonical Ltd. |
2 // Licensed under the AGPLv3, see LICENCE file for details. | 2 // Licensed under the AGPLv3, see LICENCE file for details. |
3 | 3 |
4 package main | 4 package main |
5 | 5 |
6 import ( | 6 import ( |
7 "fmt" | 7 "fmt" |
| 8 "io" |
| 9 "os" |
| 10 "os/exec" |
8 "strconv" | 11 "strconv" |
9 | 12 |
10 "launchpad.net/gnuflag" | 13 "launchpad.net/gnuflag" |
11 | 14 |
12 "launchpad.net/juju-core/cmd" | 15 "launchpad.net/juju-core/cmd" |
| 16 "launchpad.net/juju-core/environs" |
| 17 "launchpad.net/juju-core/environs/configstore" |
| 18 "launchpad.net/juju-core/juju" |
| 19 "launchpad.net/juju-core/juju/osenv" |
| 20 "launchpad.net/juju-core/provider" |
13 ) | 21 ) |
14 | 22 |
15 type DebugLogCommand struct { | 23 type DebugLogCommand struct { |
16 » cmd.CommandBase | 24 » cmd.EnvCommandBase |
17 » // The debug log command simply invokes juju ssh with the required argum
ents. | 25 |
18 » sshCmd cmd.Command | 26 » lines int |
19 » lines linesValue | 27 » filter string |
20 } | 28 } |
21 | 29 |
22 // defaultLineCount is the default number of lines to | 30 // defaultLineCount is the default number of lines to |
23 // display, from the end of the consolidated log. | 31 // display, from the end of the consolidated log. |
24 const defaultLineCount = 10 | 32 const defaultLineCount = 10 |
25 | 33 |
26 // linesValue implements gnuflag.Value, and represents | |
27 // a -n/--lines flag value compatible with "tail". | |
28 // | |
29 // A negative value (-K) corresponds to --lines=K, | |
30 // i.e. the last K lines; a positive value (+K) | |
31 // corresponds to --lines=+K, i.e. from line K onwards. | |
32 type linesValue int | |
33 | |
34 func (v *linesValue) String() string { | |
35 if *v > 0 { | |
36 return fmt.Sprintf("+%d", *v) | |
37 } | |
38 return fmt.Sprint(-*v) | |
39 } | |
40 | |
41 func (v *linesValue) Set(value string) error { | |
42 if len(value) > 0 { | |
43 sign := -1 | |
44 if value[0] == '+' { | |
45 value = value[1:] | |
46 sign = 1 | |
47 } | |
48 n, err := strconv.ParseInt(value, 10, 0) | |
49 if err == nil && n > 0 { | |
50 *v = linesValue(sign * int(n)) | |
51 return nil | |
52 } | |
53 // err is quite verbose, and doesn't convey | |
54 // any additional useful information. | |
55 } | |
56 return fmt.Errorf("invalid number of lines") | |
57 } | |
58 | |
59 const debuglogDoc = ` | 34 const debuglogDoc = ` |
60 Launch an ssh shell on the state server machine and tail the consolidated log fi
le. | 35 Stream the consolidated debug log file. This file contains the log messages |
61 The consolidated log file contains log messages from all nodes in the environmen
t. | 36 from all nodes in the environment. |
62 ` | 37 ` |
63 | 38 |
64 func (c *DebugLogCommand) Info() *cmd.Info { | 39 func (c *DebugLogCommand) Info() *cmd.Info { |
65 return &cmd.Info{ | 40 return &cmd.Info{ |
66 Name: "debug-log", | 41 Name: "debug-log", |
67 Purpose: "display the consolidated log file", | 42 Purpose: "display the consolidated log file", |
68 Doc: debuglogDoc, | 43 Doc: debuglogDoc, |
69 } | 44 } |
70 } | 45 } |
71 | 46 |
72 func (c *DebugLogCommand) SetFlags(f *gnuflag.FlagSet) { | 47 func (c *DebugLogCommand) SetFlags(f *gnuflag.FlagSet) { |
73 » c.sshCmd.SetFlags(f) | 48 » f.IntVar(&c.lines, "n", defaultLineCount, "output the last K lines; or u
se -n +K to output lines starting with the Kth") |
74 | 49 » f.IntVar(&c.lines, "lines", defaultLineCount, "") |
75 » c.lines = -defaultLineCount | 50 » f.StringVar(&c.filter, "f", "", "filter the output with a regular expres
sion") |
76 » f.Var(&c.lines, "n", "output the last K lines; or use -n +K to output li
nes starting with the Kth") | 51 » f.StringVar(&c.filter, "filter", "", "") |
77 » f.Var(&c.lines, "lines", "") | |
78 } | |
79 | |
80 func (c *DebugLogCommand) AllowInterspersedFlags() bool { | |
81 » return true | |
82 } | 52 } |
83 | 53 |
84 func (c *DebugLogCommand) Init(args []string) error { | 54 func (c *DebugLogCommand) Init(args []string) error { |
85 » tailcmd := fmt.Sprintf("tail -n %s -f /var/log/juju/all-machines.log", &
c.lines) | 55 » return nil |
86 » args = append([]string{"0"}, args...) | |
87 » args = append(args, tailcmd) | |
88 » return c.sshCmd.Init(args) | |
89 } | 56 } |
90 | 57 |
91 // Run uses "juju ssh" to log into the state server node | 58 // Run retrieves the debug log via the API. |
92 // and tails the consolidated log file which captures log | 59 func (c *DebugLogCommand) Run(ctx *cmd.Context) (err error) { |
93 // messages from all nodes. | 60 » client, err := juju.NewAPIClientFromName(c.EnvName) |
94 func (c *DebugLogCommand) Run(ctx *cmd.Context) error { | 61 » if err != nil { |
95 » return c.sshCmd.Run(ctx) | 62 » » return err |
| 63 » } |
| 64 » defer client.Close() |
| 65 |
| 66 » debugLog, err := client.WatchDebugLog(c.lines, c.filter) |
| 67 » if err != nil { |
| 68 » » logger.Infof("WatchDebugLog not supported by the API server, " + |
| 69 » » » "falling back to 1.16 compatibility mode using ssh") |
| 70 » » return c.watchDebugLog1dot16(ctx) |
| 71 » } |
| 72 » defer debugLog.Close() |
| 73 |
| 74 » _, err = io.Copy(os.Stdout, debugLog) |
| 75 » return err |
96 } | 76 } |
| 77 |
| 78 // watchDebugLog1dot16 runs in case of an older API server and uses ssh |
| 79 // but with server-side grep. |
| 80 func (c *DebugLogCommand) watchDebugLog1dot16(ctx *cmd.Context) error { |
| 81 name, local, err := c.currentEnvironment() |
| 82 if err != nil { |
| 83 return err |
| 84 } |
| 85 // Work depending on the provider. |
| 86 if local { |
| 87 // Local provider tails local log file. |
| 88 logLocation := fmt.Sprintf("%s/%s/log/all-machines.log", osenv.J
ujuHomeDir(), name) |
| 89 tailCmd := exec.Command("tail", "-n", strconv.Itoa(c.lines), "-f
", logLocation) |
| 90 grepCmd := exec.Command("grep", c.filter) |
| 91 r, w := io.Pipe() |
| 92 |
| 93 tailCmd.Stdout = w |
| 94 grepCmd.Stdin = r |
| 95 grepCmd.Stdout = os.Stdout |
| 96 grepCmd.Stderr = os.Stderr |
| 97 |
| 98 err := tailCmd.Start() |
| 99 if err != nil { |
| 100 return err |
| 101 } |
| 102 err = grepCmd.Start() |
| 103 if err != nil { |
| 104 return err |
| 105 } |
| 106 return grepCmd.Wait() |
| 107 } |
| 108 // Any other provider uses ssh with tail. |
| 109 logLocation := "/var/log/juju/all-machines.log" |
| 110 sshCmd := &SSHCommand{} |
| 111 tailGrepCmd := fmt.Sprintf("tail -n %d -f %s|grep %s", c.lines, logLocat
ion, c.filter) |
| 112 args := []string{"0", tailGrepCmd} |
| 113 err = sshCmd.Init(args) |
| 114 if err != nil { |
| 115 return err |
| 116 } |
| 117 return sshCmd.Run(ctx) |
| 118 } |
| 119 |
| 120 // currentEnvironment returns the name of the environment and if it is local. |
| 121 func (c *DebugLogCommand) currentEnvironment() (string, bool, error) { |
| 122 store, err := configstore.Default() |
| 123 if err != nil { |
| 124 return "", false, fmt.Errorf("cannot open environment info stora
ge: %v", err) |
| 125 } |
| 126 environ, err := environs.NewFromName(c.EnvironName(), store) |
| 127 if err != nil { |
| 128 return "", false, err |
| 129 } |
| 130 local := environ.Config().Type() == provider.Local |
| 131 return environ.Name(), local, nil |
| 132 } |
OLD | NEW |