Merge lp:~chipaca/snappy/logs into lp:~snappy-dev/snappy/snappy-moved-to-github

Proposed by John Lenton
Status: Merged
Approved by: Michael Vogt
Approved revision: 648
Merged at revision: 650
Proposed branch: lp:~chipaca/snappy/logs
Merge into: lp:~snappy-dev/snappy/snappy-moved-to-github
Diff against target: 494 lines (+274/-8)
6 files modified
cmd/snappy/cmd_service.go (+22/-0)
po/snappy.pot (+5/-1)
snappy/service.go (+33/-0)
snappy/service_test.go (+41/-6)
systemd/systemd.go (+89/-0)
systemd/systemd_test.go (+84/-1)
To merge this branch: bzr merge lp:~chipaca/snappy/logs
Reviewer Review Type Date Requested Status
Michael Vogt (community) Approve
Review via email: mp+270048@code.launchpad.net

Commit message

add "logs" to the service command.

Description of the change

Logs!

To post a comment you must log in.
lp:~chipaca/snappy/logs updated
647. By John Lenton

lint & etc

648. By John Lenton

found a problem with the format, fixed and added test

Revision history for this message
Michael Vogt (mvo) wrote :

Looks very nice, thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/snappy/cmd_service.go'
2--- cmd/snappy/cmd_service.go 2015-08-24 10:51:07 +0000
3+++ cmd/snappy/cmd_service.go 2015-09-03 15:54:41 +0000
4@@ -38,19 +38,23 @@
5 Restart svcRestart `command:"restart"`
6 Enable svcEnable `command:"enable"`
7 Disable svcDisable `command:"disable"`
8+ Logs svcLogs `command:"logs"`
9 }
10+
11 type svcBase struct {
12 Args struct {
13 Snap string `positional-arg-name:"snap"`
14 Service string `positional-arg-name:"service"`
15 } `positional-args:"yes"`
16 }
17+
18 type svcStatus struct{ svcBase }
19 type svcStart struct{ svcBase }
20 type svcStop struct{ svcBase }
21 type svcRestart struct{ svcBase }
22 type svcEnable struct{ svcBase }
23 type svcDisable struct{ svcBase }
24+type svcLogs struct{ svcBase }
25
26 func init() {
27 _, err := parser.AddCommand("service",
28@@ -71,6 +75,7 @@
29 doRestart
30 doEnable
31 doDisable
32+ doLogs
33 )
34
35 func (s *svcBase) doExecute(cmd int) ([]string, error) {
36@@ -82,6 +87,8 @@
37 switch cmd {
38 case doStatus:
39 return actor.Status()
40+ case doLogs:
41+ return actor.Loglines()
42 case doStart:
43 return nil, actor.Start()
44 case doStop:
45@@ -127,6 +134,21 @@
46 return err
47 }
48
49+func (s *svcLogs) Execute([]string) error {
50+ return withMutex(func() error {
51+ logs, err := s.doExecute(doLogs)
52+ if err != nil {
53+ return err
54+ }
55+
56+ for i := range logs {
57+ fmt.Println(logs[i])
58+ }
59+
60+ return nil
61+ })
62+}
63+
64 func (s *svcStart) Execute(args []string) error {
65 return withMutex(func() error {
66 _, err := s.doExecute(doStart)
67
68=== modified file 'po/snappy.pot'
69--- po/snappy.pot 2015-08-24 10:51:07 +0000
70+++ po/snappy.pot 2015-09-03 15:54:41 +0000
71@@ -7,7 +7,7 @@
72 msgid ""
73 msgstr "Project-Id-Version: snappy\n"
74 "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n"
75- "POT-Creation-Date: 2015-08-24 11:49+0100\n"
76+ "POT-Creation-Date: 2015-09-03 13:19+0100\n"
77 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
78 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
79 "Language-Team: LANGUAGE <LL@li.org>\n"
80@@ -346,6 +346,10 @@
81 msgid "unable to enable %s's service %s: %v"
82 msgstr ""
83
84+#, c-format
85+msgid "unable to get logs: %v"
86+msgstr ""
87+
88 #. TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
89 #, c-format
90 msgid "unable to start %s's service %s: %v"
91
92=== modified file 'snappy/service.go'
93--- snappy/service.go 2015-08-24 10:51:07 +0000
94+++ snappy/service.go 2015-09-03 15:54:41 +0000
95@@ -178,3 +178,36 @@
96
97 return nil
98 }
99+
100+// Logs for all found services.
101+func (actor *ServiceActor) Logs() ([]systemd.Log, error) {
102+ var svcnames []string
103+
104+ for _, svc := range actor.svcs {
105+ svcname := filepath.Base(generateServiceFileName(svc.m, *svc.svc))
106+ svcnames = append(svcnames, svcname)
107+ }
108+
109+ logs, err := actor.sysd.Logs(svcnames)
110+ if err != nil {
111+ return nil, fmt.Errorf(i18n.G("unable to get logs: %v"), err)
112+ }
113+
114+ return logs, nil
115+}
116+
117+// Loglines serializes the logs for all found services
118+func (actor *ServiceActor) Loglines() ([]string, error) {
119+ var lines []string
120+
121+ logs, err := actor.Logs()
122+ if err != nil {
123+ return nil, err
124+ }
125+
126+ for i := range logs {
127+ lines = append(lines, logs[i].String())
128+ }
129+
130+ return lines, nil
131+}
132
133=== modified file 'snappy/service_test.go'
134--- snappy/service_test.go 2015-08-24 10:51:07 +0000
135+++ snappy/service_test.go 2015-09-03 15:54:41 +0000
136@@ -35,6 +35,10 @@
137 argses [][]string
138 outs [][]byte
139 errors []error
140+ j int
141+ jsvcs [][]string
142+ jouts [][]byte
143+ jerrs []error
144 pb progress.Meter
145 }
146
147@@ -52,17 +56,38 @@
148 s.i++
149 return out, err
150 }
151+func (s *ServiceActorSuite) myJctl(svcs []string) (out []byte, err error) {
152+ s.jsvcs = append(s.jsvcs, svcs)
153+
154+ if s.j < len(s.jouts) {
155+ out = s.jouts[s.j]
156+ }
157+ if s.j < len(s.jerrs) {
158+ err = s.jerrs[s.j]
159+ }
160+ s.j++
161+
162+ return out, err
163+}
164
165 func (s *ServiceActorSuite) SetUpTest(c *C) {
166+ // force UTC timezone, for reproducible timestamps
167+ os.Setenv("TZ", "")
168+
169 SetRootDir(c.MkDir())
170 // TODO: this mkdir hack is so enable doesn't fail; remove when enable is the same as the rest
171 c.Assert(os.MkdirAll(filepath.Join(globalRootDir, "/etc/systemd/system/multi-user.target.wants"), 0755), IsNil)
172 systemd.SystemctlCmd = s.myRun
173+ systemd.JournalctlCmd = s.myJctl
174 makeInstalledMockSnap(globalRootDir, "")
175 s.i = 0
176 s.argses = nil
177 s.errors = nil
178 s.outs = nil
179+ s.j = 0
180+ s.jsvcs = nil
181+ s.jouts = nil
182+ s.jerrs = nil
183 s.pb = &MockProgressMeter{}
184 }
185
186@@ -115,6 +140,11 @@
187 nil, // status
188 &systemd.Timeout{}, // flag
189 }
190+ s.jerrs = nil
191+ s.jouts = [][]byte{
192+ []byte(`{"foo": "bar", "baz": 42}`), // for the Logs call
193+ []byte(`{"__REALTIME_TIMESTAMP":"42","MESSAGE":"hi"}`), // for the Loglines call
194+ }
195
196 c.Check(actor.Stop(), IsNil)
197 c.Check(actor.Start(), IsNil)
198@@ -125,6 +155,12 @@
199 c.Check(err, IsNil)
200 c.Assert(status, HasLen, 1)
201 c.Check(status[0], Equals, "hello-app\tsvc1\tenabled; loaded; active (running)")
202+ logs, err := actor.Logs()
203+ c.Check(err, IsNil)
204+ c.Check(logs, DeepEquals, []systemd.Log{{"foo": "bar", "baz": 42.}})
205+ lines, err := actor.Loglines()
206+ c.Check(err, IsNil)
207+ c.Check(lines, DeepEquals, []string{"1970-01-01T00:00:00.000042Z - hi"})
208 }
209
210 func (s *ServiceActorSuite) TestFindServicesReportsErrors(c *C) {
211@@ -135,12 +171,6 @@
212
213 anError := errors.New("error")
214
215- s.outs = [][]byte{
216- nil,
217- nil,
218- nil,
219- nil,
220- }
221 s.errors = []error{
222 anError, // stop
223 anError, // start
224@@ -149,6 +179,7 @@
225 anError, // disable
226 anError, // status
227 }
228+ s.jerrs = []error{anError, anError}
229
230 c.Check(actor.Stop(), NotNil)
231 c.Check(actor.Start(), NotNil)
232@@ -157,4 +188,8 @@
233 c.Check(actor.Disable(), NotNil)
234 _, err = actor.Status()
235 c.Check(err, NotNil)
236+ _, err = actor.Logs()
237+ c.Check(err, NotNil)
238+ _, err = actor.Loglines()
239+ c.Check(err, NotNil)
240 }
241
242=== modified file 'systemd/systemd.go'
243--- systemd/systemd.go 2015-08-24 10:51:07 +0000
244+++ systemd/systemd.go 2015-09-03 15:54:41 +0000
245@@ -21,11 +21,14 @@
246
247 import (
248 "bytes"
249+ "encoding/json"
250 "fmt"
251+ "io"
252 "os"
253 "os/exec"
254 "path/filepath"
255 "regexp"
256+ "strconv"
257 "strings"
258 "text/template"
259 "time"
260@@ -58,6 +61,26 @@
261 // systemctl. It's exported so it can be overridden by testing.
262 var SystemctlCmd = run
263
264+// jctl calls journalctl to get the JSON logs of the given services, wrapping the error if any.
265+func jctl(svcs []string) ([]byte, error) {
266+ cmd := []string{"journalctl", "-o", "json"}
267+
268+ for i := range svcs {
269+ cmd = append(cmd, "-u", svcs[i])
270+ }
271+
272+ bs, err := exec.Command(cmd[0], cmd[1:]...).Output() // journalctl can be messy with its stderr
273+ if err != nil {
274+ exitCode, _ := helpers.ExitCode(err)
275+ return nil, &Error{cmd: cmd, exitCode: exitCode, msg: bs}
276+ }
277+
278+ return bs, nil
279+}
280+
281+// JournalctlCmd is called from Logs to run journalctl; exported for testing.
282+var JournalctlCmd = jctl
283+
284 // Systemd exposes a minimal interface to manage systemd via the systemctl command.
285 type Systemd interface {
286 DaemonReload() error
287@@ -69,8 +92,12 @@
288 Restart(service string, timeout time.Duration) error
289 GenServiceFile(desc *ServiceDescription) string
290 Status(service string) (string, error)
291+ Logs(services []string) ([]Log, error)
292 }
293
294+// A Log is a single entry in the systemd journal
295+type Log map[string]interface{}
296+
297 // ServiceDescription describes a snappy systemd service
298 type ServiceDescription struct {
299 AppName string
300@@ -142,6 +169,38 @@
301 return err
302 }
303
304+// Logs for the given service
305+func (*systemd) Logs(serviceNames []string) ([]Log, error) {
306+ bs, err := JournalctlCmd(serviceNames)
307+ if err != nil {
308+ return nil, err
309+ }
310+
311+ const noEntries = "-- No entries --\n"
312+ if len(bs) == len(noEntries) && string(bs) == noEntries {
313+ return nil, nil
314+ }
315+
316+ var logs []Log
317+ dec := json.NewDecoder(bytes.NewReader(bs))
318+ for {
319+ var log Log
320+
321+ err = dec.Decode(&log)
322+ if err != nil {
323+ break
324+ }
325+
326+ logs = append(logs, log)
327+ }
328+
329+ if err != io.EOF {
330+ return nil, err
331+ }
332+
333+ return logs, nil
334+}
335+
336 var statusregex = regexp.MustCompile(`(?m)^(?:(.*?)=(.*))?$`)
337
338 func (s *systemd) Status(serviceName string) (string, error) {
339@@ -313,3 +372,33 @@
340 _, isTimeout := err.(*Timeout)
341 return isTimeout
342 }
343+
344+const myFmt = "2006-01-02T15:04:05.000000Z07:00"
345+
346+func (l Log) String() string {
347+ t := "-(no timestamp!)-"
348+ if ius, ok := l["__REALTIME_TIMESTAMP"]; ok {
349+ // according to systemd.journal-fields(7) it's microseconds as a decimal string
350+ sus, ok := ius.(string)
351+ if ok {
352+ if us, err := strconv.ParseInt(sus, 10, 64); err == nil {
353+ t = time.Unix(us/1000000, 1000*(us%1000000)).Format(myFmt)
354+ } else {
355+ t = fmt.Sprintf("-(timestamp not a decimal number: %#v)-", sus)
356+ }
357+ } else {
358+ t = fmt.Sprintf("-(timestamp not a string: %#v)-", ius)
359+ }
360+ }
361+
362+ sid, ok := l["SYSLOG_IDENTIFIER"].(string)
363+ if !ok {
364+ sid = "-"
365+ }
366+ msg, ok := l["MESSAGE"].(string)
367+ if !ok {
368+ msg = "-"
369+ }
370+
371+ return fmt.Sprintf("%s %s %s", t, sid, msg)
372+}
373
374=== modified file 'systemd/systemd_test.go'
375--- systemd/systemd_test.go 2015-08-24 10:51:07 +0000
376+++ systemd/systemd_test.go 2015-09-03 15:54:41 +0000
377@@ -48,22 +48,39 @@
378 argses [][]string
379 errors []error
380 outs [][]byte
381- rep *testreporter
382+
383+ j int
384+ jsvcs [][]string
385+ jouts [][]byte
386+ jerrs []error
387+
388+ rep *testreporter
389 }
390
391 var _ = Suite(&SystemdTestSuite{})
392
393 func (s *SystemdTestSuite) SetUpTest(c *C) {
394+ // force UTC timezone, for reproducible timestamps
395+ os.Setenv("TZ", "")
396+
397 SystemctlCmd = s.myRun
398 s.i = 0
399 s.argses = nil
400 s.errors = nil
401 s.outs = nil
402+
403+ JournalctlCmd = s.myJctl
404+ s.j = 0
405+ s.jsvcs = nil
406+ s.jouts = nil
407+ s.jerrs = nil
408+
409 s.rep = new(testreporter)
410 }
411
412 func (s *SystemdTestSuite) TearDownTest(c *C) {
413 SystemctlCmd = run
414+ JournalctlCmd = jctl
415 }
416
417 func (s *SystemdTestSuite) myRun(args ...string) (out []byte, err error) {
418@@ -78,6 +95,20 @@
419 return out, err
420 }
421
422+func (s *SystemdTestSuite) myJctl(svcs []string) (out []byte, err error) {
423+ s.jsvcs = append(s.jsvcs, svcs)
424+
425+ if s.j < len(s.jouts) {
426+ out = s.jouts[s.j]
427+ }
428+ if s.j < len(s.jerrs) {
429+ err = s.jerrs[s.j]
430+ }
431+ s.j++
432+
433+ return out, err
434+}
435+
436 func (s *SystemdTestSuite) errorRun(args ...string) (out []byte, err error) {
437 return nil, &Error{cmd: args, exitCode: 1, msg: []byte("error on error")}
438 }
439@@ -312,3 +343,55 @@
440 c.Check(IsTimeout(os.ErrInvalid), Equals, false)
441 c.Check(IsTimeout(&Timeout{}), Equals, true)
442 }
443+
444+func (s *SystemdTestSuite) TestLogErrJctl(c *C) {
445+ s.jerrs = []error{&Timeout{}}
446+
447+ logs, err := New("", s.rep).Logs([]string{"foo"})
448+ c.Check(err, NotNil)
449+ c.Check(logs, IsNil)
450+ c.Check(s.jsvcs, DeepEquals, [][]string{{"foo"}})
451+ c.Check(s.j, Equals, 1)
452+}
453+
454+func (s *SystemdTestSuite) TestLogErrJSON(c *C) {
455+ s.jouts = [][]byte{[]byte("this is not valid json.")}
456+
457+ logs, err := New("", s.rep).Logs([]string{"foo"})
458+ c.Check(err, NotNil)
459+ c.Check(logs, IsNil)
460+ c.Check(s.jsvcs, DeepEquals, [][]string{{"foo"}})
461+ c.Check(s.j, Equals, 1)
462+}
463+
464+func (s *SystemdTestSuite) TestLogs(c *C) {
465+ s.jouts = [][]byte{[]byte(`{"a": 1}
466+{"a": 2}
467+`)}
468+
469+ logs, err := New("", s.rep).Logs([]string{"foo"})
470+ c.Check(err, IsNil)
471+ c.Check(logs, DeepEquals, []Log{{"a": 1.}, {"a": 2.}})
472+ c.Check(s.jsvcs, DeepEquals, [][]string{{"foo"}})
473+ c.Check(s.j, Equals, 1)
474+}
475+
476+func (s *SystemdTestSuite) TestLogString(c *C) {
477+ c.Check(Log{}.String(), Equals, "-(no timestamp!)- - -")
478+ c.Check(Log{
479+ "__REALTIME_TIMESTAMP": 42,
480+ }.String(), Equals, "-(timestamp not a string: 42)- - -")
481+ c.Check(Log{
482+ "__REALTIME_TIMESTAMP": "what",
483+ }.String(), Equals, "-(timestamp not a decimal number: \"what\")- - -")
484+ c.Check(Log{
485+ "__REALTIME_TIMESTAMP": "0",
486+ "MESSAGE": "hi",
487+ }.String(), Equals, "1970-01-01T00:00:00.000000Z - hi")
488+ c.Check(Log{
489+ "__REALTIME_TIMESTAMP": "42",
490+ "MESSAGE": "hi",
491+ "SYSLOG_IDENTIFIER": "me",
492+ }.String(), Equals, "1970-01-01T00:00:00.000042Z me hi")
493+
494+}

Subscribers

People subscribed via source and target branches