Merge lp:~thumper/juju-core/debug-log-apiserver into lp:~go-bot/juju-core/trunk

Proposed by Tim Penhey
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
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.

https://codereview.appspot.com/84880044/

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.

https://codereview.appspot.com/84880044/

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

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):
   A [revision details]
   M state/apiserver/apiserver.go
   A state/apiserver/debuglog.go
   A state/apiserver/debuglog_internal_test.go
   A state/apiserver/debuglog_test.go

Revision history for this message
Andrew Wilkins (axwalk) wrote :

Haven't been through the tests yet, but here's my comments so far.

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go
File state/apiserver/debuglog.go (right):

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode39
state/apiserver/debuglog.go:39: // includeAgent -> []string - lists
agent tagsto include in the response
s/tagsto/tags to/

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode39
state/apiserver/debuglog.go:39: // includeAgent -> []string - lists
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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode40
state/apiserver/debuglog.go:40: // may finish with a '*' to match a
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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode47
state/apiserver/debuglog.go:47: // limit -> int - show this many lines
then exit
show *at most* this many lines

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode49
state/apiserver/debuglog.go:49: // replay -> string - one of [true,
false], if true, start the file from the start
replay does not suggest going back to the beginning to me. how about
rewind?

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode59
state/apiserver/debuglog.go:59: stream, err :=
newLogStream(req.URL.Query())
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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode104
state/apiserver/debuglog.go:104: return nil, fmt.Errorf("maxLines value
%q is not a valid unsigned number", value)
add the error here too?

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode113
state/apiserver/debuglog.go:113: return nil, fmt.Errorf("replay value %q
is not a valid boolean", value)
ditto

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode122
state/apiserver/debuglog.go:122: return nil, fmt.Errorf("backlog value
%q is not a valid unsigned number", value)
ditto

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode166
state/apiserver/debuglog.go:166: // authenticate parses HTTP basic
authentication and authorizes the
This is a bit out of place, and appears to be a copy-and-paste from
apiserver/httphandler.go
Can you please factor them?

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode218
state/apiserver/debuglog.go:218: if strings.HasSuffix(agent, ":") {
When does it not have the suffix?

https://codereview.appspot.com/84880044/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog_internal_test.go
File state/apiserver/debuglog_internal_test.go (right):

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog_internal_test.go#newcode272
state/apiserver/debuglog_internal_test.go:272: expected := `line 1
How about a test that has maxLines>available?

https://codereview.appspot.com/84880044/

Revision history for this message
Tim Penhey (thumper) wrote :
Download full text (4.3 KiB)

Please take a look.

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go
File state/apiserver/debuglog.go (right):

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode39
state/apiserver/debuglog.go:39: // includeAgent -> []string - lists
agent tagsto include in the response
On 2014/04/07 02:21:10, axw wrote:
> s/tagsto/tags to/

Done.

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode39
state/apiserver/debuglog.go:39: // includeAgent -> []string - lists
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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode40
state/apiserver/debuglog.go:40: // may finish with a '*' to match 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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode47
state/apiserver/debuglog.go:47: // limit -> int - show this many lines
then exit
On 2014/04/07 02:21:10, axw wrote:
> show *at most* this many lines

Done.

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode49
state/apiserver/debuglog.go:49: // replay -> string - one of [true,
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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode59
state/apiserver/debuglog.go:59: stream, err :=
newLogStream(req.URL.Query())
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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode104
state/apiserver/debuglog.go:104: return nil, fmt.Errorf("maxLines 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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode113
state/apiserver/debuglog.go:113: return nil, fmt.Errorf("replay value %q
is not a valid boolean", value)
On 2014/04/07 02:21:10, axw wrote:
> ditto

Done.

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode122
state/apiserver/debuglog.go:122: return nil, fmt.Errorf("backlog value
%q is not a valid unsigned number", value)
On 2014/04/07 02:21:...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (4.5 KiB)

On 2014/04/07 05:02:09, thumper wrote:
> Please take a look.

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go
> File state/apiserver/debuglog.go (right):

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode39
> state/apiserver/debuglog.go:39: // includeAgent -> []string - lists
agent
> tagsto include in the response
> On 2014/04/07 02:21:10, axw wrote:
> > s/tagsto/tags to/

> Done.

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode39
> state/apiserver/debuglog.go:39: // includeAgent -> []string - lists
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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode40
> state/apiserver/debuglog.go:40: // may finish with a '*' to match
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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode47
> state/apiserver/debuglog.go:47: // limit -> int - show this many
lines then
> exit
> On 2014/04/07 02:21:10, axw wrote:
> > show *at most* this many lines

> Done.

https://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode49
> state/apiserver/debuglog.go:49: // replay -> string - one of [true,
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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode59
> state/apiserver/debuglog.go:59: stream, err :=
newLogStream(req.URL.Query())
> 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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode104
> state/apiserver/debuglog.go:104: return nil, fmt.Errorf("maxLines
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://codereview.appspot.com/84880044/diff/1/state/apiserver/debuglog.go#newcode113
> state/apiserver/debuglog.go:113: return nil, fmt.Errorf("replay value
%q is not
> a valid boolean", value)
> On 2014/04/07 02:21:10, axw wrote:
> > ditto

> Done.

https://codereview.appspot.com/84880044/diff/1/state/apiserver/deb...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 := &params.ErrorResult{}
197+ if err != nil {
198+ response.Error = &params.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

Subscribers

People subscribed via source and target branches

to status/vote changes: