Merge lp:~thumper/juju-core/debug-log-apiserver into lp:~go-bot/juju-core/trunk
- debug-log-apiserver
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Tim Penhey |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2575 |
Proposed branch: | lp:~thumper/juju-core/debug-log-apiserver |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
1036 lines (+959/-14) 7 files modified
state/apiserver/apiserver.go (+10/-8) state/apiserver/charms.go (+1/-1) state/apiserver/debuglog.go (+302/-0) state/apiserver/debuglog_internal_test.go (+345/-0) state/apiserver/debuglog_test.go (+298/-0) state/apiserver/httphandler.go (+2/-4) state/apiserver/tools.go (+1/-1) |
To merge this branch: | bzr merge lp:~thumper/juju-core/debug-log-apiserver |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+214455@code.launchpad.net |
Commit message
Add debug-log server api
Add the server side end-point for the debug-log api
call. This is implemented as a websocket, with the
request parameters defining the filtering behaviour,
and the log lines that match being streamed back to
the user over the web-socket.
The first line over the websocket is the an error
structure. If there are any issues either processing
the parameters or opening the log file, there is
an error, and if not, the Error component is 'null'.
The capabilities of the server match the older python
version as closely as possible.
Description of the change
Add debug-log server api
Add the server side end-point for the debug-log api
call. This is implemented as a websocket, with the
request parameters defining the filtering behaviour,
and the log lines that match being streamed back to
the user over the web-socket.
The first line over the websocket is the an error
structure. If there are any issues either processing
the parameters or opening the log file, there is
an error, and if not, the Error component is 'null'.
The capabilities of the server match the older python
version as closely as possible.
Tim Penhey (thumper) wrote : | # |
Andrew Wilkins (axwalk) wrote : | # |
Haven't been through the tests yet, but here's my comments so far.
https:/
File state/apiserver
https:/
state/apiserver
agent tagsto include in the response
s/tagsto/tags to/
https:/
state/apiserver
agent tagsto include in the response
I think "agent" might be the wrong term here. The user cares about the
entity, not the agent responsible for it. Entity is the name we use in
state and API when passing tags around like this.
https:/
state/apiserver
prefix eg: unit-mysql-*, machine-2
"tags may finish..."
(would be nice to also begin the line with a hyphen like in the
following line, so it's clear that it's not part of the previous
sentence)
and while I'm being pedantic,
s/eg/e.g./
https:/
state/apiserver
then exit
show *at most* this many lines
https:/
state/apiserver
false], if true, start the file from the start
replay does not suggest going back to the beginning to me. how about
rewind?
https:/
state/apiserver
newLogStream(
Not a fan of constructor + init, it's too easy to overlook the need to
initialise later. Is there a good reason not to combine init() into
newLogStream?
https:/
state/apiserver
%q is not a valid unsigned number", value)
add the error here too?
https:/
state/apiserver
is not a valid boolean", value)
ditto
https:/
state/apiserver
%q is not a valid unsigned number", value)
ditto
https:/
state/apiserver
authentication and authorizes the
This is a bit out of place, and appears to be a copy-and-paste from
apiserver/
Can you please factor them?
https:/
state/apiserver
When does it not have the suffix?
Andrew Wilkins (axwalk) wrote : | # |
https:/
File state/apiserver
https:/
state/apiserver
How about a test that has maxLines>available?
Tim Penhey (thumper) wrote : | # |
Please take a look.
https:/
File state/apiserver
https:/
state/apiserver
agent tagsto include in the response
On 2014/04/07 02:21:10, axw wrote:
> s/tagsto/tags to/
Done.
https:/
state/apiserver
agent tagsto include in the response
On 2014/04/07 02:21:10, axw wrote:
> I think "agent" might be the wrong term here. The user cares about the
entity,
> not the agent responsible for it. Entity is the name we use in state
and API
> when passing tags around like this.
OK. Will change, although I imagine issues later... but we can deal with
that later.
https:/
state/apiserver
prefix eg: unit-mysql-*, machine-2
On 2014/04/07 02:21:10, axw wrote:
> "tags may finish..."
> (would be nice to also begin the line with a hyphen like in the
following line,
> so it's clear that it's not part of the previous sentence)
Done.
> and while I'm being pedantic,
> s/eg/e.g./
Done
https:/
state/apiserver
then exit
On 2014/04/07 02:21:10, axw wrote:
> show *at most* this many lines
Done.
https:/
state/apiserver
false], if true, start the file from the start
On 2014/04/07 02:21:10, axw wrote:
> replay does not suggest going back to the beginning to me. how about
rewind?
I kept 'replay' because it was what pyjuju used.
https:/
state/apiserver
newLogStream(
On 2014/04/07 02:21:10, axw wrote:
> Not a fan of constructor + init, it's too easy to overlook the need to
> initialise later. Is there a good reason not to combine init() into
> newLogStream?
Probably a bad use of 'init'. Will change to "start".
https:/
state/apiserver
%q is not a valid unsigned number", value)
On 2014/04/07 02:21:10, axw wrote:
> add the error here too?
I had wanted to provide a more useful and specific error to the caller.
But I'll add it.
https:/
state/apiserver
is not a valid boolean", value)
On 2014/04/07 02:21:10, axw wrote:
> ditto
Done.
https:/
state/apiserver
%q is not a valid unsigned number", value)
On 2014/04/07 02:21:...
Andrew Wilkins (axwalk) wrote : | # |
On 2014/04/07 05:02:09, thumper wrote:
> Please take a look.
https:/
> File state/apiserver
https:/
> state/apiserver
agent
> tagsto include in the response
> On 2014/04/07 02:21:10, axw wrote:
> > s/tagsto/tags to/
> Done.
https:/
> state/apiserver
agent
> tagsto include in the response
> On 2014/04/07 02:21:10, axw wrote:
> > I think "agent" might be the wrong term here. The user cares about
the entity,
> > not the agent responsible for it. Entity is the name we use in state
and API
> > when passing tags around like this.
> OK. Will change, although I imagine issues later... but we can deal
with that
> later.
https:/
> state/apiserver
a prefix
> eg: unit-mysql-*, machine-2
> On 2014/04/07 02:21:10, axw wrote:
> > "tags may finish..."
> > (would be nice to also begin the line with a hyphen like in the
following
> line,
> > so it's clear that it's not part of the previous sentence)
> Done.
> > and while I'm being pedantic,
> > s/eg/e.g./
> Done
https:/
> state/apiserver
lines then
> exit
> On 2014/04/07 02:21:10, axw wrote:
> > show *at most* this many lines
> Done.
https:/
> state/apiserver
false], if
> true, start the file from the start
> On 2014/04/07 02:21:10, axw wrote:
> > replay does not suggest going back to the beginning to me. how about
rewind?
> I kept 'replay' because it was what pyjuju used.
https:/
> state/apiserver
newLogStream(
> On 2014/04/07 02:21:10, axw wrote:
> > Not a fan of constructor + init, it's too easy to overlook the need
to
> > initialise later. Is there a good reason not to combine init() into
> > newLogStream?
> Probably a bad use of 'init'. Will change to "start".
https:/
> state/apiserver
value %q is
> not a valid unsigned number", value)
> On 2014/04/07 02:21:10, axw wrote:
> > add the error here too?
> I had wanted to provide a more useful and specific error to the
caller. But I'll
> add it.
https:/
> state/apiserver
%q is not
> a valid boolean", value)
> On 2014/04/07 02:21:10, axw wrote:
> > ditto
> Done.
https:/
Preview Diff
1 | === modified file 'state/apiserver/apiserver.go' |
2 | --- state/apiserver/apiserver.go 2014-04-04 16:25:01 +0000 |
3 | +++ state/apiserver/apiserver.go 2014-04-07 05:00:16 +0000 |
4 | @@ -168,14 +168,16 @@ |
5 | }() |
6 | mux := http.NewServeMux() |
7 | mux.HandleFunc("/", srv.apiHandler) |
8 | - charmHandler := &charmsHandler{httpHandler: httpHandler{state: srv.state}, dataDir: srv.dataDir} |
9 | - // charmHandler itself provides the errorSender implementation for the embedded httpHandler. |
10 | - charmHandler.httpHandler.errorSender = charmHandler |
11 | - mux.Handle("/charms", charmHandler) |
12 | - toolsHandler := &toolsHandler{httpHandler{state: srv.state}} |
13 | - // toolsHandler itself provides the errorSender implementation for the embedded httpHandler. |
14 | - toolsHandler.httpHandler.errorSender = toolsHandler |
15 | - mux.Handle("/tools", toolsHandler) |
16 | + mux.Handle("/log", |
17 | + &debugLogHandler{ |
18 | + httpHandler: httpHandler{state: srv.state}, |
19 | + logDir: srv.logDir}) |
20 | + mux.Handle("/charms", |
21 | + &charmsHandler{ |
22 | + httpHandler: httpHandler{state: srv.state}, |
23 | + dataDir: srv.dataDir}) |
24 | + mux.Handle("/tools", |
25 | + &toolsHandler{httpHandler{state: srv.state}}) |
26 | // The error from http.Serve is not interesting. |
27 | http.Serve(lis, mux) |
28 | } |
29 | |
30 | === modified file 'state/apiserver/charms.go' |
31 | --- state/apiserver/charms.go 2014-03-20 19:51:05 +0000 |
32 | +++ state/apiserver/charms.go 2014-04-07 05:00:16 +0000 |
33 | @@ -41,7 +41,7 @@ |
34 | |
35 | func (h *charmsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
36 | if err := h.authenticate(r); err != nil { |
37 | - h.authError(w) |
38 | + h.authError(w, h) |
39 | return |
40 | } |
41 | |
42 | |
43 | === added file 'state/apiserver/debuglog.go' |
44 | --- state/apiserver/debuglog.go 1970-01-01 00:00:00 +0000 |
45 | +++ state/apiserver/debuglog.go 2014-04-07 05:00:16 +0000 |
46 | @@ -0,0 +1,302 @@ |
47 | +// Copyright 2014 Canonical Ltd. |
48 | +// Licensed under the AGPLv3, see LICENCE file for details. |
49 | + |
50 | +package apiserver |
51 | + |
52 | +import ( |
53 | + "encoding/json" |
54 | + "fmt" |
55 | + "io" |
56 | + "net/http" |
57 | + "net/url" |
58 | + "os" |
59 | + "path/filepath" |
60 | + "strconv" |
61 | + "strings" |
62 | + |
63 | + "code.google.com/p/go.net/websocket" |
64 | + "github.com/juju/loggo" |
65 | + "launchpad.net/tomb" |
66 | + |
67 | + "launchpad.net/juju-core/state/api/params" |
68 | + "launchpad.net/juju-core/utils/tailer" |
69 | +) |
70 | + |
71 | +// debugLogHandler takes requests to watch the debug log. |
72 | +type debugLogHandler struct { |
73 | + httpHandler |
74 | + logDir string |
75 | +} |
76 | + |
77 | +var maxLinesReached = fmt.Errorf("max lines reached") |
78 | + |
79 | +// ServeHTTP will serve up connections as a websocket. |
80 | +// Args for the HTTP request are as follows: |
81 | +// includeEntity -> []string - lists entity tags to include in the response |
82 | +// - tags may finish with a '*' to match a prefix e.g.: unit-mysql-*, machine-2 |
83 | +// - if none are set, then all lines are considered included |
84 | +// includeModule -> []string - lists logging modules to include in the response |
85 | +// - if none are set, then all lines are considered included |
86 | +// excludeEntity -> []string - lists entity tags to exclude from the response |
87 | +// - as with include, it may finish with a '*' |
88 | +// excludeModule -> []string - lists logging modules to exclude from the response |
89 | +// limit -> uint - show *at most* this many lines |
90 | +// backtrack -> uint |
91 | +// - go back this many lines from the end before starting to filter |
92 | +// - has no meaning if 'replay' is true |
93 | +// level -> string one of [TRACE, DEBUG, INFO, WARNING, ERROR] |
94 | +// replay -> string - one of [true, false], if true, start the file from the start |
95 | +func (h *debugLogHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { |
96 | + server := websocket.Server{ |
97 | + Handler: func(socket *websocket.Conn) { |
98 | + logger.Infof("debug log handler starting") |
99 | + if err := h.authenticate(req); err != nil { |
100 | + h.sendError(socket, fmt.Errorf("auth failed: %v", err)) |
101 | + socket.Close() |
102 | + return |
103 | + } |
104 | + stream, err := newLogStream(req.URL.Query()) |
105 | + if err != nil { |
106 | + h.sendError(socket, err) |
107 | + socket.Close() |
108 | + return |
109 | + } |
110 | + // Open log file. |
111 | + logLocation := filepath.Join(h.logDir, "all-machines.log") |
112 | + logFile, err := os.Open(logLocation) |
113 | + if err != nil { |
114 | + h.sendError(socket, fmt.Errorf("cannot open log file: %v", err)) |
115 | + socket.Close() |
116 | + return |
117 | + } |
118 | + defer logFile.Close() |
119 | + |
120 | + // If we get to here, no more errors to report, so we report a nil |
121 | + // error. This way the first line of the socket is always a json |
122 | + // formatted simple error. |
123 | + if err := h.sendError(socket, nil); err != nil { |
124 | + logger.Errorf("could not send good log stream start") |
125 | + socket.Close() |
126 | + return |
127 | + } |
128 | + |
129 | + stream.start(logFile, socket) |
130 | + go func() { |
131 | + defer stream.tomb.Done() |
132 | + defer socket.Close() |
133 | + stream.tomb.Kill(stream.loop()) |
134 | + }() |
135 | + if err := stream.tomb.Wait(); err != nil { |
136 | + if err != maxLinesReached { |
137 | + logger.Errorf("debug-log handler error: %v", err) |
138 | + } |
139 | + } |
140 | + }} |
141 | + server.ServeHTTP(w, req) |
142 | +} |
143 | + |
144 | +func newLogStream(queryMap url.Values) (*logStream, error) { |
145 | + maxLines := uint(0) |
146 | + if value := queryMap.Get("maxLines"); value != "" { |
147 | + num, err := strconv.ParseUint(value, 10, 64) |
148 | + if err != nil { |
149 | + return nil, fmt.Errorf("maxLines value %q is not a valid unsigned number", value) |
150 | + } |
151 | + maxLines = uint(num) |
152 | + } |
153 | + |
154 | + fromTheStart := false |
155 | + if value := queryMap.Get("replay"); value != "" { |
156 | + replay, err := strconv.ParseBool(value) |
157 | + if err != nil { |
158 | + return nil, fmt.Errorf("replay value %q is not a valid boolean", value) |
159 | + } |
160 | + fromTheStart = replay |
161 | + } |
162 | + |
163 | + backlog := uint(0) |
164 | + if value := queryMap.Get("backlog"); value != "" { |
165 | + num, err := strconv.ParseUint(value, 10, 64) |
166 | + if err != nil { |
167 | + return nil, fmt.Errorf("backlog value %q is not a valid unsigned number", value) |
168 | + } |
169 | + backlog = uint(num) |
170 | + } |
171 | + |
172 | + level := loggo.UNSPECIFIED |
173 | + if value := queryMap.Get("level"); value != "" { |
174 | + var ok bool |
175 | + level, ok = loggo.ParseLevel(value) |
176 | + if !ok || level < loggo.TRACE || level > loggo.ERROR { |
177 | + return nil, fmt.Errorf("level value %q is not one of %q, %q, %q, %q, %q", |
178 | + value, loggo.TRACE, loggo.DEBUG, loggo.INFO, loggo.WARNING, loggo.ERROR) |
179 | + } |
180 | + } |
181 | + |
182 | + return &logStream{ |
183 | + includeEntity: queryMap["includeEntity"], |
184 | + includeModule: queryMap["includeModule"], |
185 | + excludeEntity: queryMap["excludeEntity"], |
186 | + excludeModule: queryMap["excludeModule"], |
187 | + maxLines: maxLines, |
188 | + fromTheStart: fromTheStart, |
189 | + backlog: backlog, |
190 | + filterLevel: level, |
191 | + }, nil |
192 | +} |
193 | + |
194 | +// sendError sends a JSON-encoded error response. |
195 | +func (h *debugLogHandler) sendError(w io.Writer, err error) error { |
196 | + response := ¶ms.ErrorResult{} |
197 | + if err != nil { |
198 | + response.Error = ¶ms.Error{Message: fmt.Sprint(err)} |
199 | + } |
200 | + message, err := json.Marshal(response) |
201 | + if err != nil { |
202 | + // If we are having trouble marshalling the error, we are in big trouble. |
203 | + logger.Errorf("failure to marshal SimpleError: %v", err) |
204 | + return err |
205 | + } |
206 | + message = append(message, []byte("\n")...) |
207 | + _, err = w.Write(message) |
208 | + return err |
209 | +} |
210 | + |
211 | +type logLine struct { |
212 | + line string |
213 | + agent string |
214 | + level loggo.Level |
215 | + module string |
216 | +} |
217 | + |
218 | +func parseLogLine(line string) *logLine { |
219 | + const ( |
220 | + agentField = 0 |
221 | + levelField = 3 |
222 | + moduleField = 4 |
223 | + ) |
224 | + fields := strings.Fields(line) |
225 | + result := &logLine{ |
226 | + line: line, |
227 | + } |
228 | + if len(fields) > agentField { |
229 | + agent := fields[agentField] |
230 | + if strings.HasSuffix(agent, ":") { |
231 | + result.agent = agent[:len(agent)-1] |
232 | + } |
233 | + } |
234 | + if len(fields) > moduleField { |
235 | + if level, valid := loggo.ParseLevel(fields[levelField]); valid { |
236 | + result.level = level |
237 | + result.module = fields[moduleField] |
238 | + } |
239 | + } |
240 | + |
241 | + return result |
242 | +} |
243 | + |
244 | +// logStream runs the tailer to read a log file and stream |
245 | +// it via a web socket. |
246 | +type logStream struct { |
247 | + tomb tomb.Tomb |
248 | + logTailer *tailer.Tailer |
249 | + filterLevel loggo.Level |
250 | + includeEntity []string |
251 | + includeModule []string |
252 | + excludeEntity []string |
253 | + excludeModule []string |
254 | + backlog uint |
255 | + maxLines uint |
256 | + lineCount uint |
257 | + fromTheStart bool |
258 | +} |
259 | + |
260 | +// start the tailer listening to the logFile, and sending the matching |
261 | +// lines to the writer. |
262 | +func (stream *logStream) start(logFile io.ReadSeeker, writer io.Writer) { |
263 | + if stream.fromTheStart { |
264 | + stream.logTailer = tailer.NewTailer(logFile, writer, stream.filterLine) |
265 | + } else { |
266 | + stream.logTailer = tailer.NewTailerBacktrack(logFile, writer, stream.backlog, stream.filterLine) |
267 | + } |
268 | +} |
269 | + |
270 | +// loop starts the tailer with the log file and the web socket. |
271 | +func (stream *logStream) loop() error { |
272 | + select { |
273 | + case <-stream.logTailer.Dead(): |
274 | + return stream.logTailer.Err() |
275 | + case <-stream.tomb.Dying(): |
276 | + stream.logTailer.Stop() |
277 | + } |
278 | + return nil |
279 | +} |
280 | + |
281 | +// filterLine checks the received line for one of the confgured tags. |
282 | +func (stream *logStream) filterLine(line []byte) bool { |
283 | + log := parseLogLine(string(line)) |
284 | + result := stream.checkIncludeEntity(log) && |
285 | + stream.checkIncludeModule(log) && |
286 | + !stream.exclude(log) && |
287 | + stream.checkLevel(log) |
288 | + if result && stream.maxLines > 0 { |
289 | + stream.lineCount++ |
290 | + result = stream.lineCount <= stream.maxLines |
291 | + if stream.lineCount == stream.maxLines { |
292 | + stream.tomb.Kill(maxLinesReached) |
293 | + } |
294 | + } |
295 | + return result |
296 | +} |
297 | + |
298 | +func (stream *logStream) checkIncludeEntity(line *logLine) bool { |
299 | + if len(stream.includeEntity) == 0 { |
300 | + return true |
301 | + } |
302 | + for _, value := range stream.includeEntity { |
303 | + // special handling, if ends with '*', check prefix |
304 | + if strings.HasSuffix(value, "*") { |
305 | + if strings.HasPrefix(line.agent, value[:len(value)-1]) { |
306 | + return true |
307 | + } |
308 | + } else if line.agent == value { |
309 | + return true |
310 | + } |
311 | + } |
312 | + return false |
313 | +} |
314 | + |
315 | +func (stream *logStream) checkIncludeModule(line *logLine) bool { |
316 | + if len(stream.includeModule) == 0 { |
317 | + return true |
318 | + } |
319 | + for _, value := range stream.includeModule { |
320 | + if strings.HasPrefix(line.module, value) { |
321 | + return true |
322 | + } |
323 | + } |
324 | + return false |
325 | +} |
326 | + |
327 | +func (stream *logStream) exclude(line *logLine) bool { |
328 | + for _, value := range stream.excludeEntity { |
329 | + // special handling, if ends with '*', check prefix |
330 | + if strings.HasSuffix(value, "*") { |
331 | + if strings.HasPrefix(line.agent, value[:len(value)-1]) { |
332 | + return true |
333 | + } |
334 | + } else if line.agent == value { |
335 | + return true |
336 | + } |
337 | + } |
338 | + for _, value := range stream.excludeModule { |
339 | + if strings.HasPrefix(line.module, value) { |
340 | + return true |
341 | + } |
342 | + } |
343 | + return false |
344 | +} |
345 | + |
346 | +func (stream *logStream) checkLevel(line *logLine) bool { |
347 | + return line.level >= stream.filterLevel |
348 | +} |
349 | |
350 | === added file 'state/apiserver/debuglog_internal_test.go' |
351 | --- state/apiserver/debuglog_internal_test.go 1970-01-01 00:00:00 +0000 |
352 | +++ state/apiserver/debuglog_internal_test.go 2014-04-07 05:00:16 +0000 |
353 | @@ -0,0 +1,345 @@ |
354 | +// Copyright 2014 Canonical Ltd. |
355 | +// Licensed under the AGPLv3, see LICENCE file for details. |
356 | + |
357 | +// This is an internal package test. |
358 | + |
359 | +package apiserver |
360 | + |
361 | +import ( |
362 | + "bytes" |
363 | + "io" |
364 | + "net/url" |
365 | + "os" |
366 | + "path/filepath" |
367 | + "time" |
368 | + |
369 | + "github.com/juju/loggo" |
370 | + jc "github.com/juju/testing/checkers" |
371 | + gc "launchpad.net/gocheck" |
372 | + |
373 | + "launchpad.net/juju-core/testing" |
374 | + "launchpad.net/juju-core/testing/testbase" |
375 | +) |
376 | + |
377 | +type debugInternalSuite struct { |
378 | + testbase.LoggingSuite |
379 | +} |
380 | + |
381 | +var _ = gc.Suite(&debugInternalSuite{}) |
382 | + |
383 | +func (s *debugInternalSuite) TestParseLogLine(c *gc.C) { |
384 | + line := "machine-0: 2014-03-24 22:34:25 INFO juju.cmd.jujud machine.go:127 machine agent machine-0 start (1.17.7.1-trusty-amd64 [gc])" |
385 | + logLine := parseLogLine(line) |
386 | + c.Assert(logLine.line, gc.Equals, line) |
387 | + c.Assert(logLine.agent, gc.Equals, "machine-0") |
388 | + c.Assert(logLine.level, gc.Equals, loggo.INFO) |
389 | + c.Assert(logLine.module, gc.Equals, "juju.cmd.jujud") |
390 | +} |
391 | + |
392 | +func (s *debugInternalSuite) TestParseLogLineMachineMultiline(c *gc.C) { |
393 | + line := "machine-1: continuation line" |
394 | + logLine := parseLogLine(line) |
395 | + c.Assert(logLine.line, gc.Equals, line) |
396 | + c.Assert(logLine.agent, gc.Equals, "machine-1") |
397 | + c.Assert(logLine.level, gc.Equals, loggo.UNSPECIFIED) |
398 | + c.Assert(logLine.module, gc.Equals, "") |
399 | +} |
400 | + |
401 | +func (s *debugInternalSuite) TestParseLogLineInvalid(c *gc.C) { |
402 | + line := "not a full line" |
403 | + logLine := parseLogLine(line) |
404 | + c.Assert(logLine.line, gc.Equals, line) |
405 | + c.Assert(logLine.agent, gc.Equals, "") |
406 | + c.Assert(logLine.level, gc.Equals, loggo.UNSPECIFIED) |
407 | + c.Assert(logLine.module, gc.Equals, "") |
408 | +} |
409 | + |
410 | +func checkLevel(logValue, streamValue loggo.Level) bool { |
411 | + stream := &logStream{} |
412 | + if streamValue != loggo.UNSPECIFIED { |
413 | + stream.filterLevel = streamValue |
414 | + } |
415 | + line := &logLine{level: logValue} |
416 | + return stream.checkLevel(line) |
417 | +} |
418 | + |
419 | +func (s *debugInternalSuite) TestCheckLevel(c *gc.C) { |
420 | + c.Check(checkLevel(loggo.UNSPECIFIED, loggo.UNSPECIFIED), jc.IsTrue) |
421 | + c.Check(checkLevel(loggo.TRACE, loggo.UNSPECIFIED), jc.IsTrue) |
422 | + c.Check(checkLevel(loggo.DEBUG, loggo.UNSPECIFIED), jc.IsTrue) |
423 | + c.Check(checkLevel(loggo.INFO, loggo.UNSPECIFIED), jc.IsTrue) |
424 | + c.Check(checkLevel(loggo.WARNING, loggo.UNSPECIFIED), jc.IsTrue) |
425 | + c.Check(checkLevel(loggo.ERROR, loggo.UNSPECIFIED), jc.IsTrue) |
426 | + c.Check(checkLevel(loggo.CRITICAL, loggo.UNSPECIFIED), jc.IsTrue) |
427 | + |
428 | + c.Check(checkLevel(loggo.UNSPECIFIED, loggo.TRACE), jc.IsFalse) |
429 | + c.Check(checkLevel(loggo.TRACE, loggo.TRACE), jc.IsTrue) |
430 | + c.Check(checkLevel(loggo.DEBUG, loggo.TRACE), jc.IsTrue) |
431 | + c.Check(checkLevel(loggo.INFO, loggo.TRACE), jc.IsTrue) |
432 | + c.Check(checkLevel(loggo.WARNING, loggo.TRACE), jc.IsTrue) |
433 | + c.Check(checkLevel(loggo.ERROR, loggo.TRACE), jc.IsTrue) |
434 | + c.Check(checkLevel(loggo.CRITICAL, loggo.TRACE), jc.IsTrue) |
435 | + |
436 | + c.Check(checkLevel(loggo.UNSPECIFIED, loggo.INFO), jc.IsFalse) |
437 | + c.Check(checkLevel(loggo.TRACE, loggo.INFO), jc.IsFalse) |
438 | + c.Check(checkLevel(loggo.DEBUG, loggo.INFO), jc.IsFalse) |
439 | + c.Check(checkLevel(loggo.INFO, loggo.INFO), jc.IsTrue) |
440 | + c.Check(checkLevel(loggo.WARNING, loggo.INFO), jc.IsTrue) |
441 | + c.Check(checkLevel(loggo.ERROR, loggo.INFO), jc.IsTrue) |
442 | + c.Check(checkLevel(loggo.CRITICAL, loggo.INFO), jc.IsTrue) |
443 | +} |
444 | + |
445 | +func checkIncludeEntity(logValue string, agent ...string) bool { |
446 | + stream := &logStream{includeEntity: agent} |
447 | + line := &logLine{agent: logValue} |
448 | + return stream.checkIncludeEntity(line) |
449 | +} |
450 | + |
451 | +func (s *debugInternalSuite) TestCheckIncludeEntity(c *gc.C) { |
452 | + c.Check(checkIncludeEntity("machine-0"), jc.IsTrue) |
453 | + c.Check(checkIncludeEntity("machine-0", "machine-0"), jc.IsTrue) |
454 | + c.Check(checkIncludeEntity("machine-1", "machine-0"), jc.IsFalse) |
455 | + c.Check(checkIncludeEntity("machine-1", "machine-0", "machine-1"), jc.IsTrue) |
456 | + c.Check(checkIncludeEntity("machine-0-lxc-0", "machine-0"), jc.IsFalse) |
457 | + c.Check(checkIncludeEntity("machine-0-lxc-0", "machine-0*"), jc.IsTrue) |
458 | + c.Check(checkIncludeEntity("machine-0-lxc-0", "machine-0-lxc-*"), jc.IsTrue) |
459 | +} |
460 | + |
461 | +func checkIncludeModule(logValue string, module ...string) bool { |
462 | + stream := &logStream{includeModule: module} |
463 | + line := &logLine{module: logValue} |
464 | + return stream.checkIncludeModule(line) |
465 | +} |
466 | + |
467 | +func (s *debugInternalSuite) TestCheckIncludeModule(c *gc.C) { |
468 | + c.Check(checkIncludeModule("juju"), jc.IsTrue) |
469 | + c.Check(checkIncludeModule("juju", "juju"), jc.IsTrue) |
470 | + c.Check(checkIncludeModule("juju", "juju.environ"), jc.IsFalse) |
471 | + c.Check(checkIncludeModule("juju.provisioner", "juju"), jc.IsTrue) |
472 | + c.Check(checkIncludeModule("juju.provisioner", "juju*"), jc.IsFalse) |
473 | + c.Check(checkIncludeModule("juju.provisioner", "juju.environ"), jc.IsFalse) |
474 | + c.Check(checkIncludeModule("unit.mysql/1", "juju", "unit"), jc.IsTrue) |
475 | +} |
476 | + |
477 | +func checkExcludeEntity(logValue string, agent ...string) bool { |
478 | + stream := &logStream{excludeEntity: agent} |
479 | + line := &logLine{agent: logValue} |
480 | + return stream.exclude(line) |
481 | +} |
482 | + |
483 | +func (s *debugInternalSuite) TestCheckExcludeEntity(c *gc.C) { |
484 | + c.Check(checkExcludeEntity("machine-0"), jc.IsFalse) |
485 | + c.Check(checkExcludeEntity("machine-0", "machine-0"), jc.IsTrue) |
486 | + c.Check(checkExcludeEntity("machine-1", "machine-0"), jc.IsFalse) |
487 | + c.Check(checkExcludeEntity("machine-1", "machine-0", "machine-1"), jc.IsTrue) |
488 | + c.Check(checkExcludeEntity("machine-0-lxc-0", "machine-0"), jc.IsFalse) |
489 | + c.Check(checkExcludeEntity("machine-0-lxc-0", "machine-0*"), jc.IsTrue) |
490 | + c.Check(checkExcludeEntity("machine-0-lxc-0", "machine-0-lxc-*"), jc.IsTrue) |
491 | +} |
492 | + |
493 | +func checkExcludeModule(logValue string, module ...string) bool { |
494 | + stream := &logStream{excludeModule: module} |
495 | + line := &logLine{module: logValue} |
496 | + return stream.exclude(line) |
497 | +} |
498 | + |
499 | +func (s *debugInternalSuite) TestCheckExcludeModule(c *gc.C) { |
500 | + c.Check(checkExcludeModule("juju"), jc.IsFalse) |
501 | + c.Check(checkExcludeModule("juju", "juju"), jc.IsTrue) |
502 | + c.Check(checkExcludeModule("juju", "juju.environ"), jc.IsFalse) |
503 | + c.Check(checkExcludeModule("juju.provisioner", "juju"), jc.IsTrue) |
504 | + c.Check(checkExcludeModule("juju.provisioner", "juju*"), jc.IsFalse) |
505 | + c.Check(checkExcludeModule("juju.provisioner", "juju.environ"), jc.IsFalse) |
506 | + c.Check(checkExcludeModule("unit.mysql/1", "juju", "unit"), jc.IsTrue) |
507 | +} |
508 | + |
509 | +func (s *debugInternalSuite) TestFilterLine(c *gc.C) { |
510 | + stream := &logStream{ |
511 | + filterLevel: loggo.INFO, |
512 | + includeEntity: []string{"machine-0", "unit-mysql*"}, |
513 | + includeModule: []string{"juju"}, |
514 | + excludeEntity: []string{"unit-mysql-2"}, |
515 | + excludeModule: []string{"juju.foo"}, |
516 | + } |
517 | + c.Check(stream.filterLine([]byte( |
518 | + "machine-0: date time WARNING juju")), jc.IsTrue) |
519 | + c.Check(stream.filterLine([]byte( |
520 | + "machine-1: date time WARNING juju")), jc.IsFalse) |
521 | + c.Check(stream.filterLine([]byte( |
522 | + "unit-mysql-0: date time WARNING juju")), jc.IsTrue) |
523 | + c.Check(stream.filterLine([]byte( |
524 | + "unit-mysql-1: date time WARNING juju")), jc.IsTrue) |
525 | + c.Check(stream.filterLine([]byte( |
526 | + "unit-mysql-2: date time WARNING juju")), jc.IsFalse) |
527 | + c.Check(stream.filterLine([]byte( |
528 | + "unit-wordpress-0: date time WARNING juju")), jc.IsFalse) |
529 | + c.Check(stream.filterLine([]byte( |
530 | + "machine-0: date time DEBUG juju")), jc.IsFalse) |
531 | + c.Check(stream.filterLine([]byte( |
532 | + "machine-0: date time WARNING juju.foo.bar")), jc.IsFalse) |
533 | +} |
534 | + |
535 | +func (s *debugInternalSuite) TestFilterLineWithLimit(c *gc.C) { |
536 | + stream := &logStream{ |
537 | + filterLevel: loggo.INFO, |
538 | + maxLines: 5, |
539 | + } |
540 | + line := []byte("machine-0: date time WARNING juju") |
541 | + c.Check(stream.filterLine(line), jc.IsTrue) |
542 | + c.Check(stream.filterLine(line), jc.IsTrue) |
543 | + c.Check(stream.filterLine(line), jc.IsTrue) |
544 | + c.Check(stream.filterLine(line), jc.IsTrue) |
545 | + c.Check(stream.filterLine(line), jc.IsTrue) |
546 | + c.Check(stream.filterLine(line), jc.IsFalse) |
547 | + c.Check(stream.filterLine(line), jc.IsFalse) |
548 | +} |
549 | + |
550 | +type seekWaitReader struct { |
551 | + io.ReadSeeker |
552 | + wait chan struct{} |
553 | +} |
554 | + |
555 | +func (w *seekWaitReader) Seek(offset int64, whence int) (int64, error) { |
556 | + pos, err := w.ReadSeeker.Seek(offset, whence) |
557 | + if w.wait != nil { |
558 | + close(w.wait) |
559 | + w.wait = nil |
560 | + } |
561 | + return pos, err |
562 | +} |
563 | + |
564 | +func (s *debugInternalSuite) testStreamInternal(c *gc.C, fromTheStart bool, maxLines uint, expected, errMatch string) { |
565 | + |
566 | + dir := c.MkDir() |
567 | + logPath := filepath.Join(dir, "logfile.txt") |
568 | + logFile, err := os.Create(logPath) |
569 | + c.Assert(err, gc.IsNil) |
570 | + defer logFile.Close() |
571 | + logFileReader, err := os.Open(logPath) |
572 | + c.Assert(err, gc.IsNil) |
573 | + defer logFileReader.Close() |
574 | + |
575 | + logFile.WriteString(`line 1 |
576 | +line 2 |
577 | +line 3 |
578 | +`) |
579 | + stream := &logStream{fromTheStart: fromTheStart, maxLines: maxLines} |
580 | + output := &bytes.Buffer{} |
581 | + waitReader := &seekWaitReader{logFileReader, make(chan struct{})} |
582 | + stream.start(waitReader, output) |
583 | + |
584 | + go func() { |
585 | + defer stream.tomb.Done() |
586 | + stream.tomb.Kill(stream.loop()) |
587 | + }() |
588 | + // wait for the tailer to have started tailing before writing more |
589 | + <-waitReader.wait |
590 | + |
591 | + logFile.WriteString("line 4\n") |
592 | + logFile.WriteString("line 5\n") |
593 | + |
594 | + timeout := time.After(testing.LongWait) |
595 | + for output.String() != expected { |
596 | + select { |
597 | + case <-time.After(testing.ShortWait): |
598 | + // do nothing |
599 | + case <-timeout: |
600 | + c.Fatalf("expected data didn't arrive:\n\tobtained: %#v\n\texpected: %#v", output.String(), expected) |
601 | + } |
602 | + } |
603 | + |
604 | + stream.logTailer.Stop() |
605 | + |
606 | + err = stream.tomb.Wait() |
607 | + if errMatch == "" { |
608 | + c.Assert(err, gc.IsNil) |
609 | + } else { |
610 | + c.Assert(err, gc.ErrorMatches, errMatch) |
611 | + } |
612 | +} |
613 | + |
614 | +func (s *debugInternalSuite) TestLogStreamLoopFromTheStart(c *gc.C) { |
615 | + expected := `line 1 |
616 | +line 2 |
617 | +line 3 |
618 | +line 4 |
619 | +line 5 |
620 | +` |
621 | + s.testStreamInternal(c, true, 0, expected, "") |
622 | +} |
623 | + |
624 | +func (s *debugInternalSuite) TestLogStreamLoopFromTheStartMaxLines(c *gc.C) { |
625 | + expected := `line 1 |
626 | +line 2 |
627 | +line 3 |
628 | +` |
629 | + s.testStreamInternal(c, true, 3, expected, "max lines reached") |
630 | +} |
631 | + |
632 | +func (s *debugInternalSuite) TestLogStreamLoopJustTail(c *gc.C) { |
633 | + expected := `line 4 |
634 | +line 5 |
635 | +` |
636 | + s.testStreamInternal(c, false, 0, expected, "") |
637 | +} |
638 | + |
639 | +func (s *debugInternalSuite) TestLogStreamLoopTailMaxLinesNotYetReached(c *gc.C) { |
640 | + expected := `line 4 |
641 | +line 5 |
642 | +` |
643 | + s.testStreamInternal(c, false, 3, expected, "") |
644 | +} |
645 | + |
646 | +func assertStreamParams(c *gc.C, obtained, expected *logStream) { |
647 | + c.Check(obtained.includeEntity, jc.DeepEquals, expected.includeEntity) |
648 | + c.Check(obtained.includeModule, jc.DeepEquals, expected.includeModule) |
649 | + c.Check(obtained.excludeEntity, jc.DeepEquals, expected.excludeEntity) |
650 | + c.Check(obtained.excludeModule, jc.DeepEquals, expected.excludeModule) |
651 | + c.Check(obtained.maxLines, gc.Equals, expected.maxLines) |
652 | + c.Check(obtained.fromTheStart, gc.Equals, expected.fromTheStart) |
653 | + c.Check(obtained.filterLevel, gc.Equals, expected.filterLevel) |
654 | + c.Check(obtained.backlog, gc.Equals, expected.backlog) |
655 | +} |
656 | + |
657 | +func (s *debugInternalSuite) TestNewLogStream(c *gc.C) { |
658 | + obtained, err := newLogStream(nil) |
659 | + c.Assert(err, gc.IsNil) |
660 | + assertStreamParams(c, obtained, &logStream{}) |
661 | + |
662 | + values := url.Values{ |
663 | + "includeEntity": []string{"machine-1*", "machine-2"}, |
664 | + "includeModule": []string{"juju", "unit"}, |
665 | + "excludeEntity": []string{"machine-1-lxc*"}, |
666 | + "excludeModule": []string{"juju.provisioner"}, |
667 | + "maxLines": []string{"300"}, |
668 | + "backlog": []string{"100"}, |
669 | + "level": []string{"INFO"}, |
670 | + // OK, just a little nonsense |
671 | + "replay": []string{"true"}, |
672 | + } |
673 | + expected := &logStream{ |
674 | + includeEntity: []string{"machine-1*", "machine-2"}, |
675 | + includeModule: []string{"juju", "unit"}, |
676 | + excludeEntity: []string{"machine-1-lxc*"}, |
677 | + excludeModule: []string{"juju.provisioner"}, |
678 | + maxLines: 300, |
679 | + backlog: 100, |
680 | + filterLevel: loggo.INFO, |
681 | + fromTheStart: true, |
682 | + } |
683 | + obtained, err = newLogStream(values) |
684 | + c.Assert(err, gc.IsNil) |
685 | + assertStreamParams(c, obtained, expected) |
686 | + |
687 | + _, err = newLogStream(url.Values{"maxLines": []string{"foo"}}) |
688 | + c.Assert(err, gc.ErrorMatches, `maxLines value "foo" is not a valid unsigned number`) |
689 | + |
690 | + _, err = newLogStream(url.Values{"backlog": []string{"foo"}}) |
691 | + c.Assert(err, gc.ErrorMatches, `backlog value "foo" is not a valid unsigned number`) |
692 | + |
693 | + _, err = newLogStream(url.Values{"replay": []string{"foo"}}) |
694 | + c.Assert(err, gc.ErrorMatches, `replay value "foo" is not a valid boolean`) |
695 | + |
696 | + _, err = newLogStream(url.Values{"level": []string{"foo"}}) |
697 | + c.Assert(err, gc.ErrorMatches, `level value "foo" is not one of "TRACE", "DEBUG", "INFO", "WARNING", "ERROR"`) |
698 | +} |
699 | |
700 | === added file 'state/apiserver/debuglog_test.go' |
701 | --- state/apiserver/debuglog_test.go 1970-01-01 00:00:00 +0000 |
702 | +++ state/apiserver/debuglog_test.go 2014-04-07 05:00:16 +0000 |
703 | @@ -0,0 +1,298 @@ |
704 | +// Copyright 2014 Canonical Ltd. |
705 | +// Licensed under the AGPLv3, see LICENCE file for details. |
706 | + |
707 | +package apiserver_test |
708 | + |
709 | +import ( |
710 | + "bufio" |
711 | + "crypto/tls" |
712 | + "crypto/x509" |
713 | + "encoding/base64" |
714 | + "encoding/json" |
715 | + "io" |
716 | + "net/http" |
717 | + "net/url" |
718 | + "os" |
719 | + "path/filepath" |
720 | + "strings" |
721 | + |
722 | + "code.google.com/p/go.net/websocket" |
723 | + jc "github.com/juju/testing/checkers" |
724 | + gc "launchpad.net/gocheck" |
725 | + |
726 | + "launchpad.net/juju-core/state/api/params" |
727 | + "launchpad.net/juju-core/testing" |
728 | +) |
729 | + |
730 | +type debugLogSuite struct { |
731 | + authHttpSuite |
732 | + logFile *os.File |
733 | + last int |
734 | +} |
735 | + |
736 | +var _ = gc.Suite(&debugLogSuite{}) |
737 | + |
738 | +func (s *debugLogSuite) TestWithHTTP(c *gc.C) { |
739 | + uri := s.logURL(c, "http", nil).String() |
740 | + _, err := s.sendRequest(c, "", "", "GET", uri, "", nil) |
741 | + c.Assert(err, gc.ErrorMatches, `.*malformed HTTP response.*`) |
742 | +} |
743 | + |
744 | +func (s *debugLogSuite) TestWithHTTPS(c *gc.C) { |
745 | + uri := s.logURL(c, "https", nil).String() |
746 | + response, err := s.sendRequest(c, "", "", "GET", uri, "", nil) |
747 | + c.Assert(err, gc.IsNil) |
748 | + c.Assert(response.StatusCode, gc.Equals, http.StatusBadRequest) |
749 | +} |
750 | + |
751 | +func (s *debugLogSuite) TestNoAuth(c *gc.C) { |
752 | + conn, err := s.dialWebsocketInternal(c, nil, nil) |
753 | + c.Assert(err, gc.IsNil) |
754 | + defer conn.Close() |
755 | + reader := bufio.NewReader(conn) |
756 | + |
757 | + s.assertErrorResponse(c, reader, "auth failed: invalid request format") |
758 | + s.assertWebsocketClosed(c, reader) |
759 | +} |
760 | + |
761 | +func (s *debugLogSuite) TestNoLogfile(c *gc.C) { |
762 | + reader := s.openWebsocket(c, nil) |
763 | + s.assertErrorResponse(c, reader, "cannot open log file: .*: no such file or directory") |
764 | + s.assertWebsocketClosed(c, reader) |
765 | +} |
766 | + |
767 | +func (s *debugLogSuite) TestBadParams(c *gc.C) { |
768 | + reader := s.openWebsocket(c, url.Values{"maxLines": {"foo"}}) |
769 | + s.assertErrorResponse(c, reader, `maxLines value "foo" is not a valid unsigned number`) |
770 | + s.assertWebsocketClosed(c, reader) |
771 | +} |
772 | + |
773 | +func (s *debugLogSuite) TestServesLog(c *gc.C) { |
774 | + s.ensureLogFile(c) |
775 | + reader := s.openWebsocket(c, nil) |
776 | + s.assertLogFollowing(c, reader) |
777 | + s.writeLogLines(c, logLineCount) |
778 | + |
779 | + linesRead := s.readLogLines(c, reader, logLineCount) |
780 | + c.Assert(linesRead, jc.DeepEquals, logLines) |
781 | +} |
782 | + |
783 | +func (s *debugLogSuite) TestReadsFromEnd(c *gc.C) { |
784 | + s.writeLogLines(c, 10) |
785 | + |
786 | + reader := s.openWebsocket(c, nil) |
787 | + s.assertLogFollowing(c, reader) |
788 | + s.writeLogLines(c, logLineCount) |
789 | + |
790 | + linesRead := s.readLogLines(c, reader, logLineCount-10) |
791 | + c.Assert(linesRead, jc.DeepEquals, logLines[10:]) |
792 | +} |
793 | + |
794 | +func (s *debugLogSuite) TestReplayFromStart(c *gc.C) { |
795 | + s.writeLogLines(c, 10) |
796 | + |
797 | + reader := s.openWebsocket(c, url.Values{"replay": {"true"}}) |
798 | + s.assertLogFollowing(c, reader) |
799 | + s.writeLogLines(c, logLineCount) |
800 | + |
801 | + linesRead := s.readLogLines(c, reader, logLineCount) |
802 | + c.Assert(linesRead, jc.DeepEquals, logLines) |
803 | +} |
804 | + |
805 | +func (s *debugLogSuite) TestBacklog(c *gc.C) { |
806 | + s.writeLogLines(c, 10) |
807 | + |
808 | + reader := s.openWebsocket(c, url.Values{"backlog": {"5"}}) |
809 | + s.assertLogFollowing(c, reader) |
810 | + s.writeLogLines(c, logLineCount) |
811 | + |
812 | + linesRead := s.readLogLines(c, reader, logLineCount-5) |
813 | + c.Assert(linesRead, jc.DeepEquals, logLines[5:]) |
814 | +} |
815 | + |
816 | +func (s *debugLogSuite) TestMaxLines(c *gc.C) { |
817 | + s.writeLogLines(c, 10) |
818 | + |
819 | + reader := s.openWebsocket(c, url.Values{"maxLines": {"10"}}) |
820 | + s.assertLogFollowing(c, reader) |
821 | + s.writeLogLines(c, logLineCount) |
822 | + |
823 | + linesRead := s.readLogLines(c, reader, 10) |
824 | + c.Assert(linesRead, jc.DeepEquals, logLines[10:20]) |
825 | + s.assertWebsocketClosed(c, reader) |
826 | +} |
827 | + |
828 | +func (s *debugLogSuite) TestFilter(c *gc.C) { |
829 | + s.ensureLogFile(c) |
830 | + |
831 | + reader := s.openWebsocket(c, url.Values{ |
832 | + "includeEntity": {"machine-0", "unit-ubuntu-0"}, |
833 | + "includeModule": {"juju.cmd"}, |
834 | + "excludeModule": {"juju.cmd.jujud"}, |
835 | + }) |
836 | + s.assertLogFollowing(c, reader) |
837 | + s.writeLogLines(c, logLineCount) |
838 | + |
839 | + expected := []string{logLines[0], logLines[40]} |
840 | + linesRead := s.readLogLines(c, reader, len(expected)) |
841 | + c.Assert(linesRead, jc.DeepEquals, expected) |
842 | +} |
843 | + |
844 | +func (s *debugLogSuite) readLogLines(c *gc.C, reader *bufio.Reader, count int) (linesRead []string) { |
845 | + for len(linesRead) < count { |
846 | + line, err := reader.ReadString('\n') |
847 | + c.Assert(err, gc.IsNil) |
848 | + // Trim off the trailing \n |
849 | + linesRead = append(linesRead, line[:len(line)-1]) |
850 | + } |
851 | + return linesRead |
852 | +} |
853 | + |
854 | +func (s *debugLogSuite) openWebsocket(c *gc.C, values url.Values) *bufio.Reader { |
855 | + conn, err := s.dialWebsocket(c, values) |
856 | + c.Assert(err, gc.IsNil) |
857 | + s.AddCleanup(func(_ *gc.C) { conn.Close() }) |
858 | + return bufio.NewReader(conn) |
859 | +} |
860 | + |
861 | +func (s *debugLogSuite) ensureLogFile(c *gc.C) { |
862 | + if s.logFile != nil { |
863 | + return |
864 | + } |
865 | + path := filepath.Join(s.LogDir, "all-machines.log") |
866 | + var err error |
867 | + s.logFile, err = os.Create(path) |
868 | + c.Assert(err, gc.IsNil) |
869 | + s.AddCleanup(func(c *gc.C) { |
870 | + s.logFile.Close() |
871 | + s.logFile = nil |
872 | + s.last = 0 |
873 | + }) |
874 | +} |
875 | + |
876 | +func (s *debugLogSuite) writeLogLines(c *gc.C, count int) { |
877 | + s.ensureLogFile(c) |
878 | + for i := 0; i < count && s.last < logLineCount; i++ { |
879 | + s.logFile.WriteString(logLines[s.last] + "\n") |
880 | + s.last++ |
881 | + } |
882 | +} |
883 | + |
884 | +func (s *debugLogSuite) dialWebsocketInternal(c *gc.C, queryParams url.Values, header http.Header) (*websocket.Conn, error) { |
885 | + server := s.logURL(c, "wss", queryParams).String() |
886 | + c.Logf("dialing %v", server) |
887 | + config, err := websocket.NewConfig(server, "http://localhost/") |
888 | + c.Assert(err, gc.IsNil) |
889 | + config.Header = header |
890 | + caCerts := x509.NewCertPool() |
891 | + c.Assert(caCerts.AppendCertsFromPEM([]byte(testing.CACert)), jc.IsTrue) |
892 | + config.TlsConfig = &tls.Config{RootCAs: caCerts, ServerName: "anything"} |
893 | + return websocket.DialConfig(config) |
894 | +} |
895 | + |
896 | +func (s *debugLogSuite) dialWebsocket(c *gc.C, queryParams url.Values) (*websocket.Conn, error) { |
897 | + header := http.Header{ |
898 | + "Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte(s.userTag+":"+s.password))}, |
899 | + } |
900 | + return s.dialWebsocketInternal(c, queryParams, header) |
901 | +} |
902 | + |
903 | +func (s *debugLogSuite) logURL(c *gc.C, scheme string, queryParams url.Values) *url.URL { |
904 | + _, info, err := s.APIConn.Environ.StateInfo() |
905 | + c.Assert(err, gc.IsNil) |
906 | + query := "" |
907 | + if queryParams != nil { |
908 | + query = queryParams.Encode() |
909 | + } |
910 | + return &url.URL{ |
911 | + Scheme: scheme, |
912 | + Host: info.Addrs[0], |
913 | + Path: "/log", |
914 | + RawQuery: query, |
915 | + } |
916 | +} |
917 | + |
918 | +func (s *debugLogSuite) assertWebsocketClosed(c *gc.C, reader *bufio.Reader) { |
919 | + _, err := reader.ReadByte() |
920 | + c.Assert(err, gc.Equals, io.EOF) |
921 | +} |
922 | + |
923 | +func (s *debugLogSuite) assertLogFollowing(c *gc.C, reader *bufio.Reader) { |
924 | + errResult := s.getErrorResult(c, reader) |
925 | + c.Assert(errResult.Error, gc.IsNil) |
926 | +} |
927 | + |
928 | +func (s *debugLogSuite) assertErrorResponse(c *gc.C, reader *bufio.Reader, expected string) { |
929 | + errResult := s.getErrorResult(c, reader) |
930 | + c.Assert(errResult.Error, gc.NotNil) |
931 | + c.Assert(errResult.Error.Message, gc.Matches, expected) |
932 | +} |
933 | + |
934 | +func (s *debugLogSuite) getErrorResult(c *gc.C, reader *bufio.Reader) params.ErrorResult { |
935 | + line, err := reader.ReadSlice('\n') |
936 | + c.Assert(err, gc.IsNil) |
937 | + var errResult params.ErrorResult |
938 | + err = json.Unmarshal(line, &errResult) |
939 | + c.Assert(err, gc.IsNil) |
940 | + return errResult |
941 | +} |
942 | + |
943 | +var ( |
944 | + logLines = strings.Split(` |
945 | +machine-0: 2014-03-24 22:34:25 INFO juju.cmd supercommand.go:297 running juju-1.17.7.1-trusty-amd64 [gc] |
946 | +machine-0: 2014-03-24 22:34:25 INFO juju.cmd.jujud machine.go:127 machine agent machine-0 start (1.17.7.1-trusty-amd64 [gc]) |
947 | +machine-0: 2014-03-24 22:34:25 DEBUG juju.agent agent.go:384 read agent config, format "1.18" |
948 | +machine-0: 2014-03-24 22:34:25 INFO juju.cmd.jujud machine.go:155 Starting StateWorker for machine-0 |
949 | +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "state" |
950 | +machine-0: 2014-03-24 22:34:25 INFO juju.state open.go:80 opening state; mongo addresses: ["localhost:37017"]; entity "machine-0" |
951 | +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "api" |
952 | +machine-0: 2014-03-24 22:34:25 INFO juju apiclient.go:114 state/api: dialing "wss://localhost:17070/" |
953 | +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "termination" |
954 | +machine-0: 2014-03-24 22:34:25 ERROR juju apiclient.go:119 state/api: websocket.Dial wss://localhost:17070/: dial tcp 127.0.0.1:17070: connection refused |
955 | +machine-0: 2014-03-24 22:34:25 ERROR juju runner.go:220 worker: exited "api": websocket.Dial wss://localhost:17070/: dial tcp 127.0.0.1:17070: connection refused |
956 | +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:254 worker: restarting "api" in 3s |
957 | +machine-0: 2014-03-24 22:34:25 INFO juju.state open.go:118 connection established |
958 | +machine-0: 2014-03-24 22:34:25 DEBUG juju.utils gomaxprocs.go:24 setting GOMAXPROCS to 8 |
959 | +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "local-storage" |
960 | +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "instancepoller" |
961 | +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "apiserver" |
962 | +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "resumer" |
963 | +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "cleaner" |
964 | +machine-0: 2014-03-24 22:34:25 INFO juju.state.apiserver apiserver.go:43 listening on "[::]:17070" |
965 | +machine-0: 2014-03-24 22:34:25 INFO juju runner.go:262 worker: start "minunitsworker" |
966 | +machine-0: 2014-03-24 22:34:28 INFO juju runner.go:262 worker: start "api" |
967 | +machine-0: 2014-03-24 22:34:28 INFO juju apiclient.go:114 state/api: dialing "wss://localhost:17070/" |
968 | +machine-0: 2014-03-24 22:34:28 INFO juju.state.apiserver apiserver.go:131 [1] API connection from 127.0.0.1:36491 |
969 | +machine-0: 2014-03-24 22:34:28 INFO juju apiclient.go:124 state/api: connection established |
970 | +machine-0: 2014-03-24 22:34:28 DEBUG juju.state.apiserver apiserver.go:120 <- [1] <unknown> {"RequestId":1,"Type":"Admin","Request":"Login","Params":{"AuthTag":"machine-0","Password":"ARbW7iCV4LuMugFEG+Y4e0yr","Nonce":"user-admin:bootstrap"}} |
971 | +machine-0: 2014-03-24 22:34:28 DEBUG juju.state.apiserver apiserver.go:127 -> [1] machine-0 10.305679ms {"RequestId":1,"Response":{}} Admin[""].Login |
972 | +machine-1: 2014-03-24 22:36:28 INFO juju.cmd supercommand.go:297 running juju-1.17.7.1-precise-amd64 [gc] |
973 | +machine-1: 2014-03-24 22:36:28 INFO juju.cmd.jujud machine.go:127 machine agent machine-1 start (1.17.7.1-precise-amd64 [gc]) |
974 | +machine-1: 2014-03-24 22:36:28 DEBUG juju.agent agent.go:384 read agent config, format "1.18" |
975 | +machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "api" |
976 | +machine-1: 2014-03-24 22:36:28 INFO juju apiclient.go:114 state/api: dialing "wss://10.0.3.1:17070/" |
977 | +machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "termination" |
978 | +machine-1: 2014-03-24 22:36:28 INFO juju apiclient.go:124 state/api: connection established |
979 | +machine-1: 2014-03-24 22:36:28 DEBUG juju.agent agent.go:523 writing configuration file |
980 | +machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "upgrader" |
981 | +machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "upgrade-steps" |
982 | +machine-1: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "machiner" |
983 | +machine-1: 2014-03-24 22:36:28 INFO juju.cmd.jujud machine.go:458 upgrade to 1.17.7.1-precise-amd64 already completed. |
984 | +machine-1: 2014-03-24 22:36:28 INFO juju.cmd.jujud machine.go:445 upgrade to 1.17.7.1-precise-amd64 completed. |
985 | +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju.cmd supercommand.go:297 running juju-1.17.7.1-precise-amd64 [gc] |
986 | +unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.agent agent.go:384 read agent config, format "1.18" |
987 | +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju.jujud unit.go:76 unit agent unit-ubuntu-0 start (1.17.7.1-precise-amd64 [gc]) |
988 | +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "api" |
989 | +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju apiclient.go:114 state/api: dialing "wss://10.0.3.1:17070/" |
990 | +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju apiclient.go:124 state/api: connection established |
991 | +unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.agent agent.go:523 writing configuration file |
992 | +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "upgrader" |
993 | +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "logger" |
994 | +unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.worker.logger logger.go:35 initial log config: "<root>=DEBUG" |
995 | +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "uniter" |
996 | +unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.worker.logger logger.go:60 logger setup |
997 | +unit-ubuntu-0: 2014-03-24 22:36:28 INFO juju runner.go:262 worker: start "rsyslog" |
998 | +unit-ubuntu-0: 2014-03-24 22:36:28 DEBUG juju.worker.rsyslog worker.go:76 starting rsyslog worker mode 1 for "unit-ubuntu-0" "tim-local" |
999 | +`[1:], "\n") |
1000 | + logLineCount = len(logLines) |
1001 | +) |
1002 | |
1003 | === modified file 'state/apiserver/httphandler.go' |
1004 | --- state/apiserver/httphandler.go 2014-03-13 22:47:05 +0000 |
1005 | +++ state/apiserver/httphandler.go 2014-04-07 05:00:16 +0000 |
1006 | @@ -22,8 +22,6 @@ |
1007 | |
1008 | // httpHandler handles http requests through HTTPS in the API server. |
1009 | type httpHandler struct { |
1010 | - // Structs which embed httpHandler provide their own errorSender implementation. |
1011 | - errorSender |
1012 | state *state.State |
1013 | } |
1014 | |
1015 | @@ -59,7 +57,7 @@ |
1016 | } |
1017 | |
1018 | // authError sends an unauthorized error. |
1019 | -func (h *httpHandler) authError(w http.ResponseWriter) { |
1020 | +func (h *httpHandler) authError(w http.ResponseWriter, sender errorSender) { |
1021 | w.Header().Set("WWW-Authenticate", `Basic realm="juju"`) |
1022 | - h.sendError(w, http.StatusUnauthorized, "unauthorized") |
1023 | + sender.sendError(w, http.StatusUnauthorized, "unauthorized") |
1024 | } |
1025 | |
1026 | === modified file 'state/apiserver/tools.go' |
1027 | --- state/apiserver/tools.go 2014-03-13 22:47:05 +0000 |
1028 | +++ state/apiserver/tools.go 2014-04-07 05:00:16 +0000 |
1029 | @@ -31,7 +31,7 @@ |
1030 | |
1031 | func (h *toolsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
1032 | if err := h.authenticate(r); err != nil { |
1033 | - h.authError(w) |
1034 | + h.authError(w, h) |
1035 | return |
1036 | } |
1037 |
Reviewers: mp+214455_ code.launchpad. net,
Message:
Please take a look.
Description:
Add debug-log server api
Add the server side end-point for the debug-log api
call. This is implemented as a websocket, with the
request parameters defining the filtering behaviour,
and the log lines that match being streamed back to
the user over the web-socket.
The first line over the websocket is the an error
structure. If there are any issues either processing
the parameters or opening the log file, there is
an error, and if not, the Error component is 'null'.
The capabilities of the server match the older python
version as closely as possible.
https:/ /code.launchpad .net/~thumper/ juju-core/ debug-log- apiserver/ +merge/ 214455
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/84880044/
Affected files (+971, -0 lines): /apiserver. go /debuglog. go /debuglog_ internal_ test.go /debuglog_ test.go
A [revision details]
M state/apiserver
A state/apiserver
A state/apiserver
A state/apiserver