Merge lp:~thumper/juju-core/debug-log-api into lp:~go-bot/juju-core/trunk
- debug-log-api
- Merge into trunk
Status: | Merged | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Approved by: | Tim Penhey | ||||||||||||||||||||||||
Approved revision: | no longer in the source branch. | ||||||||||||||||||||||||
Merged at revision: | 2613 | ||||||||||||||||||||||||
Proposed branch: | lp:~thumper/juju-core/debug-log-api | ||||||||||||||||||||||||
Merge into: | lp:~go-bot/juju-core/trunk | ||||||||||||||||||||||||
Diff against target: |
605 lines (+322/-131) 8 files modified
cmd/args.go (+23/-0) cmd/args_test.go (+23/-0) cmd/juju/debuglog.go (+94/-61) cmd/juju/debuglog_test.go (+149/-56) cmd/juju/main.go (+1/-1) errors/errors.go (+22/-0) errors/errors_test.go (+6/-0) state/api/client.go (+4/-13) |
||||||||||||||||||||||||
To merge this branch: | bzr merge lp:~thumper/juju-core/debug-log-api | ||||||||||||||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+215323@code.launchpad.net |
Commit message
Hook up the debug-log command line to the API.
Have 'juju debug-log' try to use the API first. If that
fails, it falls back to tailing the log over ssh.
Description of the change
Hook up the debug-log command line to the API.
Have 'juju debug-log' try to use the API first. If that
fails, it falls back to tailing the log over ssh.
Tim Penhey (thumper) wrote : | # |
Ian Booth (wallyworld) wrote : | # |
LGTM, not sure about the need for a different "api not supported"
mechanism though.
https:/
File cmd/args.go (right):
https:/
cmd/args.go:46: // f.Var(cmd.
&someMember), "name", "help")
The above comment is wrong
https:/
cmd/args.go:59: return fmt.Sprint(*v)
Could we do:
return strings.Join(*v, ",")
to be consistent with StringsValue
https:/
File cmd/args_test.go (right):
https:/
cmd/args_
I don't understand the message. Shouldn't it be "value set by args"?
https:/
File cmd/juju/
https:/
cmd/juju/
loggo.WARNING, loggo.ERROR)
Would be nice if there were a loggo helper ValidLevels()
https:/
File state/api/client.go (right):
https:/
state/api/
errors.
Till now, CodeNotImplemented has been used to flag an incompatible API
call. I'm not sure of the value of introducing something new here,
instead of just going with what's already been done.
Tim Penhey (thumper) wrote : | # |
https:/
File cmd/args.go (right):
https:/
cmd/args.go:46: // f.Var(cmd.
&someMember), "name", "help")
On 2014/04/11 00:54:43, wallyworld wrote:
> The above comment is wrong
Done.
https:/
cmd/args.go:59: return fmt.Sprint(*v)
On 2014/04/11 00:54:43, wallyworld wrote:
> Could we do:
> return strings.Join(*v, ",")
> to be consistent with StringsValue
Done.
https:/
File cmd/args_test.go (right):
https:/
cmd/args_
On 2014/04/11 00:54:43, wallyworld wrote:
> I don't understand the message. Shouldn't it be "value set by args"?
yes
https:/
File state/api/client.go (right):
https:/
state/api/
errors.
On 2014/04/11 00:54:43, wallyworld wrote:
> Till now, CodeNotImplemented has been used to flag an incompatible API
call. I'm
> not sure of the value of introducing something new here, instead of
just going
> with what's already been done.
Talked with Roger about this last night and we agreed to change it to
this.
Go Bot (go-bot) wrote : | # |
The attempt to merge lp:~thumper/juju-core/debug-log-api into lp:juju-core failed. Below is the output from the failed tests.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok l...
Preview Diff
1 | === modified file 'cmd/args.go' |
2 | --- cmd/args.go 2014-01-05 22:53:25 +0000 |
3 | +++ cmd/args.go 2014-04-11 01:07:04 +0000 |
4 | @@ -34,3 +34,26 @@ |
5 | func (v *StringsValue) String() string { |
6 | return strings.Join(*v, ",") |
7 | } |
8 | + |
9 | +// AppendStringsValue implements gnuflag.Value for a value that can be set |
10 | +// multiple times, and it appends each value to the slice. |
11 | +type AppendStringsValue []string |
12 | + |
13 | +var _ gnuflag.Value = (*AppendStringsValue)(nil) |
14 | + |
15 | +// NewAppendStringsValue is used to create the type passed into the gnuflag.FlagSet Var function. |
16 | +// f.Var(cmd.NewAppendStringsValue(&someMember), "name", "help") |
17 | +func NewAppendStringsValue(target *[]string) *AppendStringsValue { |
18 | + return (*AppendStringsValue)(target) |
19 | +} |
20 | + |
21 | +// Implements gnuflag.Value Set. |
22 | +func (v *AppendStringsValue) Set(s string) error { |
23 | + *v = append(*v, s) |
24 | + return nil |
25 | +} |
26 | + |
27 | +// Implements gnuflag.Value String. |
28 | +func (v *AppendStringsValue) String() string { |
29 | + return strings.Join(*v, ",") |
30 | +} |
31 | |
32 | === modified file 'cmd/args_test.go' |
33 | --- cmd/args_test.go 2014-01-05 22:53:25 +0000 |
34 | +++ cmd/args_test.go 2014-04-11 01:07:04 +0000 |
35 | @@ -149,3 +149,26 @@ |
36 | c.Assert(value.String(), gc.Equals, test.expected) |
37 | } |
38 | } |
39 | + |
40 | +func (*ArgsSuite) TestAppendStringsUsage(c *gc.C) { |
41 | + for i, test := range []struct { |
42 | + message string |
43 | + args []string |
44 | + expectedValue []string |
45 | + }{{ |
46 | + message: "no args", |
47 | + }, { |
48 | + message: "value set by args", |
49 | + args: []string{"--value", "foo", "--value=bar"}, |
50 | + expectedValue: []string{"foo", "bar"}, |
51 | + }} { |
52 | + c.Log(fmt.Sprintf("%v: %s", i, test.message)) |
53 | + f := gnuflag.NewFlagSet("test", gnuflag.ContinueOnError) |
54 | + f.SetOutput(ioutil.Discard) |
55 | + var value []string |
56 | + f.Var(cmd.NewAppendStringsValue(&value), "value", "help") |
57 | + err := f.Parse(false, test.args) |
58 | + c.Check(err, gc.IsNil) |
59 | + c.Check(value, gc.DeepEquals, test.expectedValue) |
60 | + } |
61 | +} |
62 | |
63 | === modified file 'cmd/juju/debuglog.go' |
64 | --- cmd/juju/debuglog.go 2013-12-13 02:18:39 +0000 |
65 | +++ cmd/juju/debuglog.go 2014-04-11 01:07:04 +0000 |
66 | @@ -1,64 +1,38 @@ |
67 | -// Copyright 2013 Canonical Ltd. |
68 | +// Copyright 2013, 2014 Canonical Ltd. |
69 | // Licensed under the AGPLv3, see LICENCE file for details. |
70 | |
71 | package main |
72 | |
73 | import ( |
74 | "fmt" |
75 | - "strconv" |
76 | + "io" |
77 | |
78 | + "github.com/juju/loggo" |
79 | "launchpad.net/gnuflag" |
80 | |
81 | "launchpad.net/juju-core/cmd" |
82 | + "launchpad.net/juju-core/cmd/envcmd" |
83 | + "launchpad.net/juju-core/errors" |
84 | + "launchpad.net/juju-core/juju" |
85 | + "launchpad.net/juju-core/state/api" |
86 | ) |
87 | |
88 | type DebugLogCommand struct { |
89 | - cmd.CommandBase |
90 | - // The debug log command simply invokes juju ssh with the required arguments. |
91 | - sshCmd cmd.Command |
92 | - lines linesValue |
93 | + envcmd.EnvCommandBase |
94 | + |
95 | + level string |
96 | + params api.DebugLogParams |
97 | } |
98 | |
99 | +var DefaultLogLocation = "/var/log/juju/all-machines.log" |
100 | + |
101 | // defaultLineCount is the default number of lines to |
102 | // display, from the end of the consolidated log. |
103 | const defaultLineCount = 10 |
104 | |
105 | -// linesValue implements gnuflag.Value, and represents |
106 | -// a -n/--lines flag value compatible with "tail". |
107 | -// |
108 | -// A negative value (-K) corresponds to --lines=K, |
109 | -// i.e. the last K lines; a positive value (+K) |
110 | -// corresponds to --lines=+K, i.e. from line K onwards. |
111 | -type linesValue int |
112 | - |
113 | -func (v *linesValue) String() string { |
114 | - if *v > 0 { |
115 | - return fmt.Sprintf("+%d", *v) |
116 | - } |
117 | - return fmt.Sprint(-*v) |
118 | -} |
119 | - |
120 | -func (v *linesValue) Set(value string) error { |
121 | - if len(value) > 0 { |
122 | - sign := -1 |
123 | - if value[0] == '+' { |
124 | - value = value[1:] |
125 | - sign = 1 |
126 | - } |
127 | - n, err := strconv.ParseInt(value, 10, 0) |
128 | - if err == nil && n > 0 { |
129 | - *v = linesValue(sign * int(n)) |
130 | - return nil |
131 | - } |
132 | - // err is quite verbose, and doesn't convey |
133 | - // any additional useful information. |
134 | - } |
135 | - return fmt.Errorf("invalid number of lines") |
136 | -} |
137 | - |
138 | const debuglogDoc = ` |
139 | -Launch an ssh shell on the state server machine and tail the consolidated log file. |
140 | -The consolidated log file contains log messages from all nodes in the environment. |
141 | +Stream the consolidated debug log file. This file contains the log messages |
142 | +from all nodes in the environment. |
143 | ` |
144 | |
145 | func (c *DebugLogCommand) Info() *cmd.Info { |
146 | @@ -70,27 +44,86 @@ |
147 | } |
148 | |
149 | func (c *DebugLogCommand) SetFlags(f *gnuflag.FlagSet) { |
150 | - c.sshCmd.SetFlags(f) |
151 | - |
152 | - c.lines = -defaultLineCount |
153 | - f.Var(&c.lines, "n", "output the last K lines; or use -n +K to output lines starting with the Kth") |
154 | - f.Var(&c.lines, "lines", "") |
155 | -} |
156 | - |
157 | -func (c *DebugLogCommand) AllowInterspersedFlags() bool { |
158 | - return true |
159 | + c.EnvCommandBase.SetFlags(f) |
160 | + |
161 | + f.Var(cmd.NewAppendStringsValue(&c.params.IncludeEntity), "i", "only show log messages for these entities") |
162 | + f.Var(cmd.NewAppendStringsValue(&c.params.IncludeEntity), "include", "only show log messages for these entities") |
163 | + f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeEntity), "x", "only show log messages for these entities") |
164 | + f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeEntity), "exclude", "only show log messages for these entities") |
165 | + f.Var(cmd.NewAppendStringsValue(&c.params.IncludeModule), "include-module", "only show log messages for these logging modules") |
166 | + f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeModule), "exclude-module", "do not show log messages for these logging modules") |
167 | + |
168 | + f.StringVar(&c.level, "l", "", "log level to show, one of [TRACE, DEBUG, INFO, WARNING, ERROR]") |
169 | + f.StringVar(&c.level, "level", "", "") |
170 | + |
171 | + f.UintVar(&c.params.Backlog, "n", defaultLineCount, "go back this many lines from the end before starting to filter") |
172 | + f.UintVar(&c.params.Backlog, "lines", defaultLineCount, "") |
173 | + f.UintVar(&c.params.Limit, "limit", 0, "show at most this many lines") |
174 | + f.BoolVar(&c.params.Replay, "replay", false, "start filtering from the start") |
175 | } |
176 | |
177 | func (c *DebugLogCommand) Init(args []string) error { |
178 | - tailcmd := fmt.Sprintf("tail -n %s -f /var/log/juju/all-machines.log", &c.lines) |
179 | - args = append([]string{"0"}, args...) |
180 | - args = append(args, tailcmd) |
181 | - return c.sshCmd.Init(args) |
182 | -} |
183 | - |
184 | -// Run uses "juju ssh" to log into the state server node |
185 | -// and tails the consolidated log file which captures log |
186 | -// messages from all nodes. |
187 | -func (c *DebugLogCommand) Run(ctx *cmd.Context) error { |
188 | - return c.sshCmd.Run(ctx) |
189 | + err := c.EnvCommandBase.Init() |
190 | + if err != nil { |
191 | + return err |
192 | + } |
193 | + if c.level != "" { |
194 | + level, ok := loggo.ParseLevel(c.level) |
195 | + if !ok || level < loggo.TRACE || level > loggo.ERROR { |
196 | + return fmt.Errorf("level value %q is not one of %q, %q, %q, %q, %q", |
197 | + c.level, loggo.TRACE, loggo.DEBUG, loggo.INFO, loggo.WARNING, loggo.ERROR) |
198 | + } |
199 | + c.params.Level = level |
200 | + } |
201 | + return cmd.CheckEmpty(args) |
202 | +} |
203 | + |
204 | +type DebugLogAPI interface { |
205 | + WatchDebugLog(params api.DebugLogParams) (io.ReadCloser, error) |
206 | + Close() error |
207 | +} |
208 | + |
209 | +var getDebugLogAPI = func(envName string) (DebugLogAPI, error) { |
210 | + return juju.NewAPIClientFromName(envName) |
211 | +} |
212 | + |
213 | +// Run retrieves the debug log via the API. |
214 | +func (c *DebugLogCommand) Run(ctx *cmd.Context) (err error) { |
215 | + client, err := getDebugLogAPI(c.EnvName) |
216 | + if err != nil { |
217 | + return err |
218 | + } |
219 | + defer client.Close() |
220 | + debugLog, err := client.WatchDebugLog(c.params) |
221 | + if err != nil { |
222 | + if errors.IsNotSupportedError(err) { |
223 | + return c.watchDebugLog1dot18(ctx) |
224 | + } |
225 | + return err |
226 | + } |
227 | + defer debugLog.Close() |
228 | + _, err = io.Copy(ctx.Stdout, debugLog) |
229 | + return err |
230 | +} |
231 | + |
232 | +var runSSHCommand = func(sshCmd *SSHCommand, ctx *cmd.Context) error { |
233 | + return sshCmd.Run(ctx) |
234 | +} |
235 | + |
236 | +// watchDebugLog1dot18 runs in case of an older API server and uses ssh |
237 | +// but with server-side grep. |
238 | +func (c *DebugLogCommand) watchDebugLog1dot18(ctx *cmd.Context) error { |
239 | + ctx.Infof("Server does not support new stream log, falling back to tail") |
240 | + ctx.Verbosef("filters are not supported with tail") |
241 | + sshCmd := &SSHCommand{} |
242 | + tailCmd := fmt.Sprintf("tail -n -%d -f %s", c.params.Backlog, DefaultLogLocation) |
243 | + // If the api doesn't support WatchDebugLog, then it won't be running in |
244 | + // HA either, so machine 0 is where it is all at. |
245 | + args := []string{"0", tailCmd} |
246 | + err := sshCmd.Init(args) |
247 | + if err != nil { |
248 | + return err |
249 | + } |
250 | + sshCmd.EnvName = c.EnvName |
251 | + return runSSHCommand(sshCmd, ctx) |
252 | } |
253 | |
254 | === modified file 'cmd/juju/debuglog_test.go' |
255 | --- cmd/juju/debuglog_test.go 2014-03-11 09:29:33 +0000 |
256 | +++ cmd/juju/debuglog_test.go 2014-04-11 01:07:04 +0000 |
257 | @@ -4,72 +4,165 @@ |
258 | package main |
259 | |
260 | import ( |
261 | + "fmt" |
262 | + "io" |
263 | + "io/ioutil" |
264 | + "strings" |
265 | + |
266 | + "github.com/juju/loggo" |
267 | + jc "github.com/juju/testing/checkers" |
268 | gc "launchpad.net/gocheck" |
269 | |
270 | "launchpad.net/juju-core/cmd" |
271 | + "launchpad.net/juju-core/errors" |
272 | + "launchpad.net/juju-core/state/api" |
273 | "launchpad.net/juju-core/testing" |
274 | ) |
275 | |
276 | type DebugLogSuite struct { |
277 | + testing.FakeHomeSuite |
278 | } |
279 | |
280 | var _ = gc.Suite(&DebugLogSuite{}) |
281 | |
282 | -func runDebugLog(c *gc.C, args ...string) (*DebugLogCommand, error) { |
283 | - cmd := &DebugLogCommand{ |
284 | - sshCmd: &dummySSHCommand{}, |
285 | - } |
286 | - _, err := testing.RunCommand(c, cmd, args) |
287 | - return cmd, err |
288 | -} |
289 | - |
290 | -type dummySSHCommand struct { |
291 | - SSHCommand |
292 | - runCalled bool |
293 | -} |
294 | - |
295 | -func (c *dummySSHCommand) Run(ctx *cmd.Context) error { |
296 | - c.runCalled = true |
297 | +func (s *DebugLogSuite) TestArgParsing(c *gc.C) { |
298 | + for i, test := range []struct { |
299 | + args []string |
300 | + expected api.DebugLogParams |
301 | + errMatch string |
302 | + }{ |
303 | + { |
304 | + expected: api.DebugLogParams{ |
305 | + Backlog: 10, |
306 | + }, |
307 | + }, { |
308 | + args: []string{"-n0"}, |
309 | + }, { |
310 | + args: []string{"--lines=50"}, |
311 | + expected: api.DebugLogParams{ |
312 | + Backlog: 50, |
313 | + }, |
314 | + }, { |
315 | + args: []string{"-l", "foo"}, |
316 | + errMatch: `level value "foo" is not one of "TRACE", "DEBUG", "INFO", "WARNING", "ERROR"`, |
317 | + }, { |
318 | + args: []string{"--level=INFO"}, |
319 | + expected: api.DebugLogParams{ |
320 | + Backlog: 10, |
321 | + Level: loggo.INFO, |
322 | + }, |
323 | + }, { |
324 | + args: []string{"--include", "machine-1", "-i", "machine-2"}, |
325 | + expected: api.DebugLogParams{ |
326 | + IncludeEntity: []string{"machine-1", "machine-2"}, |
327 | + Backlog: 10, |
328 | + }, |
329 | + }, { |
330 | + args: []string{"--exclude", "machine-1", "-x", "machine-2"}, |
331 | + expected: api.DebugLogParams{ |
332 | + ExcludeEntity: []string{"machine-1", "machine-2"}, |
333 | + Backlog: 10, |
334 | + }, |
335 | + }, { |
336 | + args: []string{"--include-module", "juju.foo", "--include-module", "unit"}, |
337 | + expected: api.DebugLogParams{ |
338 | + IncludeModule: []string{"juju.foo", "unit"}, |
339 | + Backlog: 10, |
340 | + }, |
341 | + }, { |
342 | + args: []string{"--exclude-module", "juju.foo", "--exclude-module", "unit"}, |
343 | + expected: api.DebugLogParams{ |
344 | + ExcludeModule: []string{"juju.foo", "unit"}, |
345 | + Backlog: 10, |
346 | + }, |
347 | + }, { |
348 | + args: []string{"--replay"}, |
349 | + expected: api.DebugLogParams{ |
350 | + Backlog: 10, |
351 | + Replay: true, |
352 | + }, |
353 | + }, { |
354 | + args: []string{"--limit", "100"}, |
355 | + expected: api.DebugLogParams{ |
356 | + Backlog: 10, |
357 | + Limit: 100, |
358 | + }, |
359 | + }, |
360 | + } { |
361 | + c.Logf("test %v", i) |
362 | + command := &DebugLogCommand{} |
363 | + err := testing.InitCommand(command, test.args) |
364 | + if test.errMatch == "" { |
365 | + c.Check(err, gc.IsNil) |
366 | + c.Check(command.params, jc.DeepEquals, test.expected) |
367 | + } else { |
368 | + c.Check(err, gc.ErrorMatches, test.errMatch) |
369 | + } |
370 | + } |
371 | +} |
372 | + |
373 | +func (s *DebugLogSuite) TestParamsPassed(c *gc.C) { |
374 | + fake := &fakeDebugLogAPI{} |
375 | + s.PatchValue(&getDebugLogAPI, func(envName string) (DebugLogAPI, error) { |
376 | + return fake, nil |
377 | + }) |
378 | + _, err := testing.RunCommand(c, &DebugLogCommand{}, []string{ |
379 | + "-i", "machine-1*", "-x", "machine-1-lxc-1", |
380 | + "--include-module=juju.provisioner", |
381 | + "--lines=500", |
382 | + "--level=WARNING", |
383 | + }) |
384 | + c.Assert(err, gc.IsNil) |
385 | + c.Assert(fake.params, gc.DeepEquals, api.DebugLogParams{ |
386 | + IncludeEntity: []string{"machine-1*"}, |
387 | + IncludeModule: []string{"juju.provisioner"}, |
388 | + ExcludeEntity: []string{"machine-1-lxc-1"}, |
389 | + Backlog: 500, |
390 | + Level: loggo.WARNING, |
391 | + }) |
392 | +} |
393 | + |
394 | +func (s *DebugLogSuite) TestLogOutput(c *gc.C) { |
395 | + s.PatchValue(&getDebugLogAPI, func(envName string) (DebugLogAPI, error) { |
396 | + return &fakeDebugLogAPI{log: "this is the log output"}, nil |
397 | + }) |
398 | + ctx, err := testing.RunCommand(c, &DebugLogCommand{}, nil) |
399 | + c.Assert(err, gc.IsNil) |
400 | + c.Assert(testing.Stdout(ctx), gc.Equals, "this is the log output") |
401 | +} |
402 | + |
403 | +func (s *DebugLogSuite) TestTailFallback(c *gc.C) { |
404 | + s.PatchValue(&runSSHCommand, func(sshCmd *SSHCommand, ctx *cmd.Context) error { |
405 | + fmt.Fprintf(ctx.Stdout, "%s", sshCmd.Args) |
406 | + return nil |
407 | + }) |
408 | + s.PatchValue(&getDebugLogAPI, func(envName string) (DebugLogAPI, error) { |
409 | + return &fakeDebugLogAPI{err: errors.NewNotSupportedError("testing")}, nil |
410 | + }) |
411 | + ctx, err := testing.RunCommand(c, &DebugLogCommand{}, []string{"-n", "100"}) |
412 | + c.Assert(err, gc.IsNil) |
413 | + c.Check(testing.Stderr(ctx), gc.Equals, "Server does not support new stream log, falling back to tail\n") |
414 | + c.Check(testing.Stdout(ctx), gc.Equals, "[tail -n -100 -f /var/log/juju/all-machines.log]") |
415 | +} |
416 | + |
417 | +func newFakeDebugLogAPI(log string) DebugLogAPI { |
418 | + return &fakeDebugLogAPI{log: log} |
419 | +} |
420 | + |
421 | +type fakeDebugLogAPI struct { |
422 | + log string |
423 | + params api.DebugLogParams |
424 | + err error |
425 | +} |
426 | + |
427 | +func (fake *fakeDebugLogAPI) WatchDebugLog(params api.DebugLogParams) (io.ReadCloser, error) { |
428 | + if fake.err != nil { |
429 | + return nil, fake.err |
430 | + } |
431 | + fake.params = params |
432 | + return ioutil.NopCloser(strings.NewReader(fake.log)), nil |
433 | +} |
434 | + |
435 | +func (fake *fakeDebugLogAPI) Close() error { |
436 | return nil |
437 | } |
438 | - |
439 | -// debug-log is implemented by invoking juju ssh with the correct arguments. |
440 | -// This test helper checks for the expected invocation. |
441 | -func (s *DebugLogSuite) assertDebugLogInvokesSSHCommand(c *gc.C, expected string, args ...string) { |
442 | - defer testing.MakeSampleHome(c).Restore() |
443 | - debugLogCmd, err := runDebugLog(c, args...) |
444 | - c.Assert(err, gc.IsNil) |
445 | - debugCmd := debugLogCmd.sshCmd.(*dummySSHCommand) |
446 | - c.Assert(debugCmd.runCalled, gc.Equals, true) |
447 | - c.Assert(debugCmd.Target, gc.Equals, "0") |
448 | - c.Assert([]string{expected}, gc.DeepEquals, debugCmd.Args) |
449 | -} |
450 | - |
451 | -const logLocation = "/var/log/juju/all-machines.log" |
452 | - |
453 | -func (s *DebugLogSuite) TestDebugLog(c *gc.C) { |
454 | - const expected = "tail -n 10 -f " + logLocation |
455 | - s.assertDebugLogInvokesSSHCommand(c, expected) |
456 | -} |
457 | - |
458 | -func (s *DebugLogSuite) TestDebugLogFrom(c *gc.C) { |
459 | - const expected = "tail -n +1 -f " + logLocation |
460 | - s.assertDebugLogInvokesSSHCommand(c, expected, "-n", "+1") |
461 | - s.assertDebugLogInvokesSSHCommand(c, expected, "--lines=+1") |
462 | -} |
463 | - |
464 | -func (s *DebugLogSuite) TestDebugLogLast(c *gc.C) { |
465 | - const expected = "tail -n 100 -f " + logLocation |
466 | - s.assertDebugLogInvokesSSHCommand(c, expected, "-n", "100") |
467 | - s.assertDebugLogInvokesSSHCommand(c, expected, "--lines=100") |
468 | -} |
469 | - |
470 | -func (s *DebugLogSuite) TestDebugLogValidation(c *gc.C) { |
471 | - defer testing.MakeSampleHome(c).Restore() |
472 | - _, err := runDebugLog(c, "-n", "0") |
473 | - c.Assert(err, gc.ErrorMatches, "invalid value \"0\" for flag -n: invalid number of lines") |
474 | - _, err = runDebugLog(c, "-n", "-1") |
475 | - c.Assert(err, gc.ErrorMatches, "invalid value \"-1\" for flag -n: invalid number of lines") |
476 | - _, err = runDebugLog(c, "-n", "fnord") |
477 | - c.Assert(err, gc.ErrorMatches, "invalid value \"fnord\" for flag -n: invalid number of lines") |
478 | -} |
479 | |
480 | === modified file 'cmd/juju/main.go' |
481 | --- cmd/juju/main.go 2014-03-31 00:12:13 +0000 |
482 | +++ cmd/juju/main.go 2014-04-11 01:07:04 +0000 |
483 | @@ -96,7 +96,7 @@ |
484 | jujucmd.Register(wrap(&SCPCommand{})) |
485 | jujucmd.Register(wrap(&SSHCommand{})) |
486 | jujucmd.Register(wrap(&ResolvedCommand{})) |
487 | - jujucmd.Register(wrap(&DebugLogCommand{sshCmd: &SSHCommand{}})) |
488 | + jujucmd.Register(wrap(&DebugLogCommand{})) |
489 | jujucmd.Register(wrap(&DebugHooksCommand{})) |
490 | jujucmd.Register(wrap(&RetryProvisioningCommand{})) |
491 | |
492 | |
493 | === modified file 'errors/errors.go' |
494 | --- errors/errors.go 2014-04-08 10:20:15 +0000 |
495 | +++ errors/errors.go 2014-04-11 01:07:04 +0000 |
496 | @@ -127,3 +127,25 @@ |
497 | _, ok := err.(*alreadyExistsError) |
498 | return ok |
499 | } |
500 | + |
501 | +type notSupportedError struct { |
502 | + what string |
503 | +} |
504 | + |
505 | +// NewNotSupportedError returns an error signifying that something is not |
506 | +// supported. For example a client API call to a server that does not support |
507 | +// the action. |
508 | +func NewNotSupportedError(what string) error { |
509 | + return ¬SupportedError{what: what} |
510 | +} |
511 | + |
512 | +func (e *notSupportedError) Error() string { |
513 | + return e.what + " not supported" |
514 | +} |
515 | + |
516 | +// IsNotSupportedError reports whether the error |
517 | +// was created with NewNotSupportedError. |
518 | +func IsNotSupportedError(err error) bool { |
519 | + _, ok := err.(*notSupportedError) |
520 | + return ok |
521 | +} |
522 | |
523 | === modified file 'errors/errors_test.go' |
524 | --- errors/errors_test.go 2014-04-08 10:20:15 +0000 |
525 | +++ errors/errors_test.go 2014-04-11 01:07:04 +0000 |
526 | @@ -38,11 +38,13 @@ |
527 | isUnauthorizedError := &errorSatisfier{errors.IsUnauthorizedError} |
528 | isNotImplementedError := &errorSatisfier{errors.IsNotImplementedError} |
529 | isAlreadyExistsError := &errorSatisfier{errors.IsAlreadyExistsError} |
530 | + isNotSupportedError := &errorSatisfier{errors.IsNotSupportedError} |
531 | satisfiers := []*errorSatisfier{ |
532 | isNotFoundError, |
533 | isUnauthorizedError, |
534 | isNotImplementedError, |
535 | isAlreadyExistsError, |
536 | + isNotSupportedError, |
537 | } |
538 | |
539 | // make some errors, and record the errorSatsifier |
540 | @@ -88,6 +90,10 @@ |
541 | errors.NewAlreadyExistsError("something"), |
542 | "something already exists", |
543 | isAlreadyExistsError, |
544 | + }, { |
545 | + errors.NewNotSupportedError("something"), |
546 | + "something not supported", |
547 | + isNotSupportedError, |
548 | }} |
549 | |
550 | for i, t := range errorTests { |
551 | |
552 | === modified file 'state/api/client.go' |
553 | --- state/api/client.go 2014-04-10 10:00:50 +0000 |
554 | +++ state/api/client.go 2014-04-11 01:07:04 +0000 |
555 | @@ -20,6 +20,7 @@ |
556 | |
557 | "launchpad.net/juju-core/charm" |
558 | "launchpad.net/juju-core/constraints" |
559 | + "launchpad.net/juju-core/errors" |
560 | "launchpad.net/juju-core/instance" |
561 | "launchpad.net/juju-core/state/api/params" |
562 | "launchpad.net/juju-core/tools" |
563 | @@ -725,16 +726,6 @@ |
564 | return websocket.DialConfig(config) |
565 | } |
566 | |
567 | -type connectionError struct { |
568 | - error |
569 | -} |
570 | - |
571 | -// IsConnectionError reports whether the error is a connection error. |
572 | -func IsConnectionError(err error) bool { |
573 | - _, ok := err.(*connectionError) |
574 | - return ok |
575 | -} |
576 | - |
577 | // DebugLogParams holds parameters for WatchDebugLog that control the |
578 | // filtering of the log messages. If the structure is zero initialized, the |
579 | // entire log file is sent back starting from the end, and until the user |
580 | @@ -772,14 +763,14 @@ |
581 | // WatchDebugLog returns a ReadCloser that the caller can read the log lines |
582 | // from. Only log lines that match the filtering specified in the |
583 | // DebugLogParams are returned. It returns an error that satisfies |
584 | -// IsConnectionError when the connection cannot be made. |
585 | +// IsNotSupportedError when the API server does not support the end-point. |
586 | func (c *Client) WatchDebugLog(args DebugLogParams) (io.ReadCloser, error) { |
587 | // The websocket connection just hangs if the server doesn't have the log |
588 | // end point. So do a version check, as version was added at the same time |
589 | // as the remote end point. |
590 | _, err := c.AgentVersion() |
591 | if err != nil { |
592 | - return nil, &connectionError{fmt.Errorf("server doesn't support debug log websocket")} |
593 | + return nil, errors.NewNotSupportedError("WatchDebugLog") |
594 | } |
595 | // Prepare URL. |
596 | attrs := url.Values{} |
597 | @@ -811,7 +802,7 @@ |
598 | cfg.TlsConfig = &tls.Config{RootCAs: c.st.certPool, ServerName: "anything"} |
599 | connection, err := websocketDialConfig(cfg) |
600 | if err != nil { |
601 | - return nil, &connectionError{err} |
602 | + return nil, err |
603 | } |
604 | // Read the initial error and translate to a real error. |
605 | // Read up to the first new line character. We can't use bufio here as it |
Reviewers: mp+215323_ code.launchpad. net,
Message:
Please take a look.
Description:
Hook up the debug-log command line to the API.
Have 'juju debug-log' try to use the API first. If that
fails, it falls back to tailing the log over ssh.
https:/ /code.launchpad .net/~thumper/ juju-core/ debug-log- api/+merge/ 215323
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/85570044/
Affected files (+325, -131 lines): debuglog. go debuglog_ test.go errors_ test.go
A [revision details]
M cmd/args.go
M cmd/args_test.go
M cmd/juju/
M cmd/juju/
M cmd/juju/main.go
M errors/errors.go
M errors/
M state/api/client.go