Merge lp:~zyga/lava-core/history-and-logging into lp:lava-core

Proposed by Zygmunt Krynicki
Status: Merged
Merged at revision: 8
Proposed branch: lp:~zyga/lava-core/history-and-logging
Merge into: lp:lava-core
Diff against target: 956 lines (+929/-2)
4 files modified
lava/core/history.py (+522/-0)
lava/core/logging.py (+120/-0)
lava/core/tests/test_history.py (+278/-0)
setup.py (+9/-2)
To merge this branch: bzr merge lp:~zyga/lava-core/history-and-logging
Reviewer Review Type Date Requested Status
Linaro Validation Team Pending
Review via email: mp+104882@code.launchpad.net

Description of the change

This is a re-spin of earlier merge proposal (https://code.launchpad.net/~zkrynicki/lava-core/logging-mixin/+merge/102339)

Again, to quote my extensive commit message:

  Add the logging and history modules

  Both modules cooperate on centralized logging interface and have largely
  identical APIs. LoggingMixIn class adds self.history and self.logging
  properties to the class it is injected to.

  Both properties return proxy objects that ultimately call classic python
  logging and lava-core'esque history. History is much like logging but has two
  advantages over plain logging.

  The first advantage is that it was designed up front for being hierarchical.
  This naturally constructs a tree of activities. I believe that such structure
  can be visualized much better than a plain log file ever could. Any composite
  operation also computes and records the duration of all the activities it
  contains. This yields much more transparent duration awareness of entire
  processes that lava performs internally.

  The second advantage is that it can carry more data than regular python logging
  can. All arguments to the message template are passed to the string.format()
  function as keywords. This allows us to transparently provide key-value data in
  alongside each history entry. This makes automatic analysis and data retrieval
  much easier. It also allows for new data logging features such as downloading a
  file and storing the speed history as an array in the activity meta-data. Such
  object can be rendered with appropriate care by the front end thus making the
  user interface far more rich than we could previously attempt without extreme
  hacks and side-band, custom communication channels. Another feature that takes
  advantage of this rich meta-data is attachments. Any activity can have a
  collection of attachments that can be presented in a web front-end (attachments
  also carry explicit MIME type). LAVA Core currently uses this to ensure we
  capture full, uncorrupted output of any external programs we run.

  The history module offers utility functions that can render any activity (or
  the convenience lava.core.history.history.top activity which records all known
  history of the process) as JSON or plain text (in several formats). In addition
  both formats can be saved, along with all the attachments, in a history
  tarball. This functionality is exploited in the top-level lava command line
  program, that allows the user to record the history of any LAVA commands, for
  visualization, debugging, rich analysis or other, yet-unknown purpose.

To post a comment you must log in.
9. By Zygmunt Krynicki

Fix module docstrings not to refer to lava.core.utils anymore

10. By Zygmunt Krynicki

Update docstrings of misc logging functions

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :
Download full text (29.2 KiB)

[And once more, to the merge proposal as well :(]

Hi. This overall seems reasonably nice. With things like this I always
wonder if we'll ever get around to building the nice UIs that are
enabled...

Some general concerns:

1) Incremental output on the scheduler job page is _massively_ useful
   and losing it even temporarily is not acceptable, IMHO. I don't
   quite see how this meshes with this code. I guess we could still
   define a logging handler that outputs log messages to stdout/stderr
   and that will display nicely in the scheduler view until we get
   around to doing something more sophisticated there.

2) I can see having two sets of debug/info/warning/error functions with
   slightly different APIs (% vs .format() interpolation) being trying.
   But maybe this will be a non-issue -- I guess usually it the code
   will be saying self.history.info() or self.logger.info() and the
   logger vs history API will be clear.

3) I'm just a little worried by memory consumption -- it seems that
   there is the potential to end up with a _lot_ of objects hanging
   around, particularly if you hang on to tracebacks. It might be worth
   stringifying things a little earlier. I guess this won't affect the
   API and so can be done later...

On Mon, 07 May 2012 09:49:18 -0000, Zygmunt Krynicki <email address hidden> wrote:
> Zygmunt Krynicki has proposed merging lp:~zkrynicki/lava-core/history-and-logging into lp:lava-core.
>
> Requested reviews:
> Linaro Validation Team (linaro-validation)
>
> For more details, see:
> https://code.launchpad.net/~zkrynicki/lava-core/history-and-logging/+merge/104882
>
> This is a re-spin of earlier merge proposal (https://code.launchpad.net/~zkrynicki/lava-core/logging-mixin/+merge/102339)
>
> Again, to quote my extensive commit message:
>
> Add the logging and history modules
>
> Both modules cooperate on centralized logging interface and have largely
> identical APIs. LoggingMixIn class adds self.history and self.logging
> properties to the class it is injected to.
>
> Both properties return proxy objects that ultimately call classic python
> logging and lava-core'esque history. History is much like logging but has two
> advantages over plain logging.
>
> The first advantage is that it was designed up front for being hierarchical.
> This naturally constructs a tree of activities. I believe that such structure
> can be visualized much better than a plain log file ever could. Any composite
> operation also computes and records the duration of all the activities it
> contains. This yields much more transparent duration awareness of entire
> processes that lava performs internally.
>
> The second advantage is that it can carry more data than regular python logging
> can. All arguments to the message template are passed to the string.format()
> function as keywords. This allows us to transparently provide key-value data in
> alongside each history entry. This makes automatic analysis and data retrieval
> much easier. It also allows for new data logging features such as downloading a
> file and storing the speed history as an array in the acti...

Revision history for this message
Zygmunt Krynicki (zyga) wrote :
Download full text (36.3 KiB)

> [And once more, to the merge proposal as well :(]
>
> Hi. This overall seems reasonably nice. With things like this I always
> wonder if we'll ever get around to building the nice UIs that are
> enabled...
>
> Some general concerns:
>
> 1) Incremental output on the scheduler job page is _massively_ useful
> and losing it even temporarily is not acceptable, IMHO. I don't
> quite see how this meshes with this code. I guess we could still
> define a logging handler that outputs log messages to stdout/stderr
> and that will display nicely in the scheduler view until we get
> around to doing something more sophisticated there.

While I was somewhat skeptical about this, anything involving fast models made me reconsider quickly. I did a small proof of concept that streams data in real time (anything invoked via the agent) while retaining stdout/stderr separation. So I guess that means we'll just keep the verbose output for the current scheduler view. In the future we may replace that with real-time status updates so that we'd get the full hierarchical structure but as you rightfully point out, that may take a while.

> 2) I can see having two sets of debug/info/warning/error functions with
> slightly different APIs (% vs .format() interpolation) being trying.
> But maybe this will be a non-issue -- I guess usually it the code
> will be saying self.history.info() or self.logger.info() and the
> logger vs history API will be clear.

We can still reconsider this as it's reasonably easy/fast to replace this with the moderate amount of usage it's seen so far. My motivations for format() were twofold: python deprecates % in favor of .format() and (perhaps I have over-valued this) I wanted to have key-value meta-data in each log message, believing that it could be used by some future smart UI to present the data better. If we were to go with % formatting then we don't really loose key-value formats (they just look different). Perhaps we could do a machine substitution so that we can get consistent logging + % API and keep the true data behind the scenes. In practical terms it would do mean calling self.history.info("%(something)s", something="blarg"). What do you think?

> 3) I'm just a little worried by memory consumption -- it seems that
> there is the potential to end up with a _lot_ of objects hanging
> around, particularly if you hang on to tracebacks. It might be worth
> stringifying things a little earlier. I guess this won't affect the
> API and so can be done later...

I thought about it too, by demo-2 I made sure we don't keep an Activity record for each stdout line we print. In theory this dispatcher is better as it never keeps an in-memory log file of any test. The amount of 'activities' is pretty much constant across all test runs. Still, unlike logging which never suffers from this problem, activities do add up. When it becomes a problem we can simply record them (in some machine format) as they are being generated. The only issue here is that activities that are in flight (__enter__()) will require seeking + updates to "close" and any exceptions we generate could need special treatment (as ...

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :
Download full text (40.5 KiB)

On Tue, 08 May 2012 10:11:19 -0000, Zygmunt Krynicki <email address hidden> wrote:
> > [And once more, to the merge proposal as well :(]
> >
> > Hi. This overall seems reasonably nice. With things like this I always
> > wonder if we'll ever get around to building the nice UIs that are
> > enabled...
> >
> > Some general concerns:
> >
> > 1) Incremental output on the scheduler job page is _massively_ useful
> > and losing it even temporarily is not acceptable, IMHO. I don't
> > quite see how this meshes with this code. I guess we could still
> > define a logging handler that outputs log messages to stdout/stderr
> > and that will display nicely in the scheduler view until we get
> > around to doing something more sophisticated there.
>
> While I was somewhat skeptical about this, anything involving fast
> models made me reconsider quickly.

I don't think fast models are fundamentally different here, but
whatever, I'm glad I don't have to convince you :-)

> I did a small proof of concept that streams data in real time
> (anything invoked via the agent) while retaining stdout/stderr
> separation. So I guess that means we'll just keep the verbose output
> for the current scheduler view. In the future we may replace that with
> real-time status updates so that we'd get the full hierarchical
> structure but as you rightfully point out, that may take a while.

Cool.

> > 2) I can see having two sets of debug/info/warning/error functions with
> > slightly different APIs (% vs .format() interpolation) being trying.
> > But maybe this will be a non-issue -- I guess usually it the code
> > will be saying self.history.info() or self.logger.info() and the
> > logger vs history API will be clear.
>
> We can still reconsider this as it's reasonably easy/fast to replace
> this with the moderate amount of usage it's seen so far. My
> motivations for format() were twofold: python deprecates % in favor of
> .format() and (perhaps I have over-valued this) I wanted to have
> key-value meta-data in each log message, believing that it could be
> used by some future smart UI to present the data better. If we were to
> go with % formatting then we don't really loose key-value formats
> (they just look different). Perhaps we could do a machine substitution
> so that we can get consistent logging + % API and keep the true data
> behind the scenes. In practical terms it would do mean calling
> self.history.info("%(something)s", something="blarg"). What do you
> think?

I'm not sure that preserving the key/value pairs that are intended to be
part of the formatted log message will be much of a win really (because
its intended to be formatted, you'll likely do things like pass a field
value rather than the whole object, for example, and there's not really
much value in having a simple value both inside and outside the log
message). I can see the potential desire to attach other data to the
Activity objects though.

So, um, I don't know. But also, I don't think it's a very big deal...

> > 3) I'm just a little worried by memory consumption -- it seems that
> > there is the potential to end up with a _lot_ of objects hangi...

11. By Zygmunt Krynicki

Fix History.last

12. By Zygmunt Krynicki

Simplify DictProxy

As pointed out by reviewers, DictProxy can be somewhat simple with the correct
usage of __setattr__ in __init__ and usage of __getattr__ instead of
__getattribute__

I've also added __eq__ and __repr__ for convenience

13. By Zygmunt Krynicki

Allow Activity.attach() to work on plain file names

This change is backported from demo-3 branch. It allows one to
attach files to an activity simply by passing their pathname.

14. By Zygmunt Krynicki

Improve and fix _build_json

There are three separate changes here:

Early unicode correctness:
 * all strings are converted to unicode with str.decode("UTF-8")
 * all dictionary keys are converted to unicode with __unicode__()
 * all non-native objects are converted to unicode with __unicode__()

Support for datetime and timedelta:
 Previously it was a small burden on the caller, now timedelta and datetime
 objects are consistently encoded to strings with a well-known representation.

Support for tuples:
 Previously tuples would not work, now they are treated just like lists. In
 addition, both lists, tuples and dictionaries are removed from the (dead)
 code path for 'native' objects as they are already handled earlier.

15. By Zygmunt Krynicki

Simplify Activity.as_json()

This change takes advantage of the improved _build_json() that already does the
necessary conversions and 'if obj is None' checks

16. By Zygmunt Krynicki

Add depencency declaration.

This is a small backport from future branches to allow unit tests to work

17. By Zygmunt Krynicki

Add tests for lava.core.history

Most of the Activity class is tested. Part of the json serialization is tested.
DictProxy and History objects are not tested yet

18. By Zygmunt Krynicki

Fix a bug in Hisstory.last

History.last returned None just after constructing the History object. It
should have returned 'top' (root of the history) instead.

19. By Zygmunt Krynicki

Add unit tests for History class

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lava/core/history.py'
2--- lava/core/history.py 1970-01-01 00:00:00 +0000
3+++ lava/core/history.py 2012-05-16 11:20:24 +0000
4@@ -0,0 +1,522 @@
5+# Copyright (C) 2011-2012 Linaro Limited
6+# vim: set fileencoding=utf8 :
7+#
8+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
9+#
10+# This file is part of lava-core
11+#
12+# lava-core is free software: you can redistribute it and/or modify
13+# it under the terms of the GNU Lesser General Public License version 3
14+# as published by the Free Software Foundation
15+#
16+# lava-core is distributed in the hope that it will be useful,
17+# but WITHOUT ANY WARRANTY; without even the implied warranty of
18+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+# GNU General Public License for more details.
20+#
21+# You should have received a copy of the GNU Lesser General Public License
22+# along with lava-core. If not, see <http://www.gnu.org/licenses/>.
23+
24+from __future__ import absolute_import, print_function
25+
26+"""
27+lava.core.history
28+=================
29+
30+Implementation of hierarhical history
31+"""
32+
33+from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
34+import collections
35+import datetime
36+import os
37+import sys
38+import tarfile
39+import traceback
40+
41+from json_document.serializers import JSON
42+from json_schema_validator.extensions import (
43+ datetime_extension, timedelta_extension)
44+import simplejson
45+
46+
47+class DictProxy(object):
48+
49+ def __init__(self, d):
50+ object.__setattr__(self, "_DictProxy__d", d)
51+
52+ def as_dict(self):
53+ return self.__d
54+
55+ def __repr__(self):
56+ return "<DictProxy {0!r}>".format(self.__d)
57+
58+ def __setattr__(self, name, value):
59+ self.__d[name] = value
60+
61+ def __getattr__(self, name):
62+ try:
63+ return self.__d[name]
64+ except KeyError:
65+ raise AttributeError(name)
66+
67+ def __eq__(self, other):
68+ if isinstance(other, DictProxy):
69+ return self.__d == other.__d
70+ else:
71+ return False
72+
73+
74+ExceptionInfo = collections.namedtuple(
75+ "ExceptionInfo", "filename lineno function text")
76+
77+
78+_name_and_back = {
79+ DEBUG: "DEBUG",
80+ INFO: "INFO",
81+ WARNING: "WARNING",
82+ ERROR: "ERROR",
83+ CRITICAL: "CRITICAL",
84+ "DEBUG": DEBUG,
85+ "INFO": INFO,
86+ "WARNING": WARNING,
87+ "ERROR": ERROR,
88+ "CRITICAL": CRITICAL
89+}
90+
91+
92+class Activity(object):
93+
94+ def __init__(self, level, message, **meta):
95+ """
96+ Create a new activity with the specified message, level and
97+ initial meta-data
98+ """
99+ # Message that has format() references to meta-data
100+ self._level = level
101+ self._message = message
102+ self._meta = DictProxy(meta)
103+ # File attachments
104+ self._attachments = []
105+ # Children activities
106+ self._sub_activities = []
107+ # Start and end timestamps
108+ self._start = datetime.datetime.utcnow()
109+ self._end = self._start
110+ # Exception + traceback
111+ self._exc_value = None
112+ self._exc_type = None
113+ self._traceback = None
114+
115+ @property
116+ def raw_message(self):
117+ """
118+ the raw, unformatted message
119+ """
120+ return self._message
121+
122+ @property
123+ def message(self):
124+ """
125+ rendered message
126+
127+ Rendering substitutes str.format() style variables with data from
128+ self.meta.
129+ """
130+ return self._message.format(**self._meta.as_dict())
131+
132+ @property
133+ def level(self):
134+ """
135+ level number, 10 (DEBUG) - 50 (CRITICAL)
136+ """
137+ return self._level
138+
139+ @property
140+ def level_name(self):
141+ """
142+ name of the level "DEBUG" ... "CRITICAL"
143+ """
144+ return _name_and_back[self._level]
145+
146+ @property
147+ def meta(self):
148+ """
149+ collection of meta-data
150+
151+ Meta-data serves two purposes: to fill in data to the message
152+ template and to store arbitrary auxiliary data. This property
153+ returns a dictionary proxy that looks like an object (has normal
154+ attribute accessors) but still talks to the real dictionary behind
155+ the scenes. You can both read and write/set attributes at will.
156+ """
157+ return self._meta
158+
159+ def attach(self, stream_or_pathname, name=None, mime_type='text/plain'):
160+ """
161+ Add an attachment.
162+
163+ Everything is extracted from the file-like object stream. If you care
164+ about a particular name of the attachment it an be specified with name.
165+ MIME type can help the web front-end to render the content. It defaults
166+ to text/plain which is suitable for most log files.
167+ """
168+ if isinstance(stream_or_pathname, basestring):
169+ orig_name = stream_or_pathname
170+ else:
171+ orig_name = stream_or_pathname.name
172+ if name is None:
173+ name = os.path.basename(orig_name)
174+ self._attachments.append(
175+ DictProxy({
176+ 'name': name,
177+ 'orig_name': orig_name,
178+ 'mime_type': mime_type}))
179+
180+ @property
181+ def duration(self):
182+ """
183+ Duration of the activity.
184+
185+ If the activity has not started the duration is always zero.
186+ Activities that are in progress will observe the duration to grow.
187+ """
188+ if self._start is None:
189+ return datetime.timedelta()
190+ if self._end is None:
191+ return datetime.datetime.utcnow() - self._start
192+ return self._end - self._start
193+
194+ def as_json(self):
195+ """
196+ Convert this Activity to JSON
197+ """
198+ obj = simplejson.OrderedDict((
199+ ("level", self.level_name),
200+ ("message", self._message),
201+ ("meta", self.meta.as_dict()),
202+ ("start", self._start),
203+ ("duration", self.duration),
204+ ("end", self._end),
205+ ("exc_value", self._exc_value),
206+ ("exc_type", (self._exc_type.__name__
207+ if self._exc_type else None)),
208+ ("traceback", ([ExceptionInfo(*ei) for ei in
209+ traceback.extract_tb(self._traceback)]
210+ if self._traceback else None)),
211+ ("attachments", [
212+ attachment.as_dict() for attachment in self._attachments]),
213+ ("sub_activities", [
214+ activity.as_json() for activity in self._sub_activities])))
215+ return _build_json(obj)
216+
217+ def __str__(self):
218+ """"
219+ Returns self.message
220+ """
221+ return self.message
222+
223+ def save_as_tarball(self, scratch, tarball_stream, mode='w:gz'):
224+ """
225+ Save activity history as a tarball.
226+
227+ The tarball will have two files 'history.txt' and 'history.json' with
228+ appropriate encoding of this activity, as well as all sub-activities.
229+ Any attachments are added as well.
230+ """
231+ with tarfile.open(tarball_stream.name, mode=mode,
232+ fileobj=tarball_stream) as tarball:
233+ with scratch.open('history.txt') as stream:
234+ self.print_(stream=stream)
235+ tarball.add(stream.name, 'history.txt')
236+ with scratch.open('history.json') as stream:
237+ JSON.dump(stream, self.as_json())
238+ tarball.add(stream.name, 'history.json')
239+ self._save_tarball_attachments(tarball)
240+
241+ def _save_tarball_attachments(self, tarball):
242+ for attachment in self._attachments:
243+ if os.path.isfile(attachment.orig_name):
244+ tarball.add(attachment.orig_name, attachment.name)
245+ for activity in self._sub_activities:
246+ activity._save_tarball_attachments(tarball)
247+
248+ def __repr__(self):
249+ return "<Activity: %r>" % str(self)
250+
251+ def __enter__(self):
252+ """
253+ Begins a 'long' activity.
254+
255+ Long activities have both start and end, and thus, non-zero duration.
256+ This is called automatically by History.__enter__ so you won't usually
257+ have to use it explicitly.
258+
259+ Here we reset self.end to None (it was initialized to 'now' in
260+ __init__)
261+ """
262+ self._end = None
263+ return self
264+
265+ def __exit__(self, exc_type, exc_value, traceback):
266+ """
267+ Terminates a 'long' activity.
268+
269+ Here we set self.end to 'now' and record any exception information.
270+ See __enter__() for more information on 'long' activities.
271+ """
272+ self._end = datetime.datetime.utcnow()
273+ self._exc_value = exc_value
274+ self._exc_type = exc_type
275+ self._traceback = traceback
276+
277+ def print_(self, stream=None, print_meta=False, print_children=True,
278+ stack=None):
279+ """
280+ Print a nice Unicode tree of activities.
281+
282+ This is very useful for tracing and debugging.
283+ """
284+ if stack is None:
285+ stack = []
286+ if stream is None:
287+ stream = sys.stdout
288+ fragments = []
289+ if print_meta is False:
290+ fragments.append("{0:7}".format(self.level_name))
291+ fragments.append(" | ")
292+ fragments.append("{start}.{micro:06}".format(
293+ start=self._start.strftime("%Y-%m-%d %T"),
294+ micro=self._start.microsecond))
295+ fragments.append(" | ")
296+ if self.duration.total_seconds() > 0:
297+ fragments.append("{0:14}s".format(self.duration))
298+ else:
299+ fragments.append("{0:15}".format(""))
300+ fragments.append(" | ")
301+ fragments.append(''.join(self._tree_indent(stack)))
302+ # Process the message to make it safer
303+ message = self.message
304+ message = message.replace("\n", " ").replace("\r", " ")
305+ if isinstance(message, unicode):
306+ message = message.encode(stream.encoding or "UTF-8", 'replace')
307+ fragments.append(message)
308+ if self._exc_value:
309+ fragments.append(", ***crashed*** {0}".format(self._exc_value))
310+ print("".join(fragments), file=stream)
311+ if print_meta:
312+ print("\tstart: {0}".format(self._start))
313+ if self.duration.total_seconds() > 0:
314+ print("\tduration: {0}".format(self.duration))
315+ if self._end is not None and self._end != self._start:
316+ print("\tend: {0}".format(self._end))
317+ if self._exc_value is not None:
318+ print("\texc_value: {0}".format(self._exc_value))
319+ if self._exc_type is not None:
320+ print("\texc_type: {0}".format(self._exc_type))
321+ if self._traceback is not None:
322+ for index, exc_info in enumerate(self._traceback):
323+ print("\ttraceback.{0}.filename: {1}".format(
324+ index, self._traceback.filename))
325+ print("\ttraceback.{0}.lineno: {1}".format(
326+ index, self._traceback.lineno))
327+ print("\ttraceback.{0}.function {1}".format(
328+ index, self._traceback.function))
329+ print("\ttraceback.{0}.text {1}".format(
330+ index, self._traceback.text))
331+ for key, value in self.meta.as_dict().iteritems():
332+ # Skip one special key used by logging integration
333+ if key == "__prefmt_msg":
334+ continue
335+ print("\t{key}: {value!r}".format(key=key, value=value))
336+ if print_children:
337+ pos = len(stack)
338+ stack.append(len(self._sub_activities))
339+ for sub in self._sub_activities:
340+ stack[pos] -= 1
341+ sub.print_(stream, print_meta, print_children, stack)
342+ stack.pop()
343+
344+ @staticmethod
345+ def _tree_indent(stack, use_unicode=False):
346+ """
347+ Compute the indentation part of the tree displayed by print_.
348+ """
349+ # The code here might be tricky. It's easier to understand if you keep
350+ # in mind that stack[n] tells you how many more entries are left in the
351+ # tree at that stack depth. The stack is constructed by print_()
352+ for items_left in stack[:-1]:
353+ # FIXME: this is broekn
354+ if items_left == 0:
355+ yield " "
356+ elif items_left == 1:
357+ if use_unicode:
358+ yield "└ "
359+ else:
360+ yield "\ "
361+ elif items_left > 1:
362+ if use_unicode:
363+ yield "│ "
364+ else:
365+ yield "| "
366+ for items_left in stack[-1:]:
367+ if items_left == 0:
368+ if use_unicode:
369+ yield "└── "
370+ else:
371+ yield "\-- "
372+ elif items_left > 0:
373+ if use_unicode:
374+ yield "├── "
375+ else:
376+ yield "|-- "
377+
378+
379+def _build_json(obj):
380+ """
381+ Helper to construct json that keeps ordering and discards None values
382+ """
383+ if isinstance(obj, (dict, simplejson.OrderedDict)):
384+ new_obj = simplejson.OrderedDict()
385+ for key, value in obj.iteritems():
386+ if value is None or value == [] or value == {}:
387+ continue
388+ else:
389+ new_obj[unicode(key)] = _build_json(value)
390+ return new_obj
391+ elif isinstance(obj, (tuple, list)):
392+ return [_build_json(item) for item in obj]
393+ elif (isinstance(obj, (int, float, unicode, bool))
394+ or obj is None):
395+ return obj
396+ elif isinstance(obj, str):
397+ return obj.decode("UTF-8")
398+ elif isinstance(obj, datetime.datetime):
399+ return datetime_extension.to_json(obj)
400+ elif isinstance(obj, datetime.timedelta):
401+ return timedelta_extension.to_json(obj)
402+ else:
403+ try:
404+ return unicode(obj)
405+ except Exception:
406+ raise ValueError("cannot convert {0!r} to JSON".format(obj))
407+
408+
409+class History(object):
410+ """
411+ History class, provides helper methods for hierarchical logging.
412+ Maintains a stack of activities that are nested with context
413+ management.
414+ """
415+
416+ def __init__(self, top):
417+ """
418+ Create a History that starts with the specified 'top' activity
419+
420+ Usually you don't want to call this. Instead, use the singleton
421+ instance 'history'.
422+ """
423+ self._nesting = [top]
424+ self._last = top
425+
426+ @property
427+ def top(self):
428+ """
429+ the top-level activity
430+
431+ Very useful to grab entire history and do something with it, for
432+ example history.top.as_json() or history.top.print_()
433+ """
434+ return self._nesting[0]
435+
436+ @property
437+ def bottom(self):
438+ """
439+ the bottommost activity
440+
441+ This is the most nested activity, as established by:
442+
443+ with history.xxx():
444+ ...
445+ """
446+ return self._nesting[-1]
447+
448+ @property
449+ def last(self):
450+ """
451+ the most recent activity
452+
453+ This is the most recently created activity in this history.
454+ """
455+ return self._last
456+
457+ def log(self, level, message, **meta):
458+ """
459+ Create an activity and append it to the history
460+ """
461+ self._last = Activity(level, message, **meta)
462+ self.bottom._sub_activities.append(self._last)
463+ return self
464+
465+ def debug(self, message, **meta):
466+ """
467+ Append DEBUG activity to the bottommost activity
468+ """
469+ return self.log(DEBUG, message, **meta)
470+
471+ def info(self, message, **meta):
472+ """
473+ Append INFO activity to the bottommost activity
474+ """
475+ return self.log(INFO, message, **meta)
476+
477+ def warning(self, message, **meta):
478+ """
479+ Append WARNING activity to the bottommost activity
480+ """
481+ return self.log(WARNING, message, **meta)
482+
483+ def error(self, message, **meta):
484+ """
485+ Append ERROR activity to the bottommost activity
486+ """
487+ return self.log(ERROR, message, **meta)
488+
489+ def critical(self, message, **meta):
490+ """
491+ Append CRITICAL activity to the bottommost activity
492+ """
493+ return self.log(CRITICAL, message, **meta)
494+
495+ def __enter__(self):
496+ """
497+ Begins nested activity
498+
499+ Nesting is widely useful as it helps to create a structure in the
500+ history stream. Typically you will call this method indirectly
501+ with code that looks like this:
502+
503+ with history.info("Doing something complex"):
504+ history.debug("step 1")
505+ history.debug("step 2")
506+ history.debug("step 3")
507+
508+ Here we also call __enter__() on the last activity to convert it to a
509+ 'long' activity (one that has both start, end and non-zero duration)
510+ """
511+ self._nesting.append(self._last)
512+ return self._last.__enter__()
513+
514+ def __exit__(self, exc_type, exc_value, traceback):
515+ """
516+ Terminates most recently nested activity
517+
518+ Here we also call __exit__() on the last activity to allow it to record
519+ any exceptions that may have been raised.
520+ """
521+ last = self._nesting.pop()
522+ last.__exit__(exc_type, exc_value, traceback)
523+
524+
525+# Global history
526+history = History(Activity(DEBUG, "History module loaded"))
527
528=== added file 'lava/core/logging.py'
529--- lava/core/logging.py 1970-01-01 00:00:00 +0000
530+++ lava/core/logging.py 2012-05-16 11:20:24 +0000
531@@ -0,0 +1,120 @@
532+# Copyright (C) 2011-2012 Linaro Limited
533+# vim: set fileencoding=utf8 :
534+#
535+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
536+#
537+# This file is part of lava-core
538+#
539+# lava-core is free software: you can redistribute it and/or modify
540+# it under the terms of the GNU Lesser General Public License version 3
541+# as published by the Free Software Foundation
542+#
543+# lava-core is distributed in the hope that it will be useful,
544+# but WITHOUT ANY WARRANTY; without even the implied warranty of
545+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
546+# GNU General Public License for more details.
547+#
548+# You should have received a copy of the GNU Lesser General Public License
549+# along with lava-core. If not, see <http://www.gnu.org/licenses/>.
550+
551+from __future__ import absolute_import
552+
553+"""
554+lava.core.logging
555+=================
556+
557+Logging utilities
558+"""
559+
560+import logging
561+
562+from lava.core.history import history as global_history
563+
564+
565+class LoggerToHistoryProxy(object):
566+ """
567+ Wrapper that looks like logging.Logger but also creates history records
568+ """
569+
570+ _history = global_history
571+
572+ def __init__(self, logger):
573+ self._logger = logger
574+
575+ def log(self, level, msg, *args, **kwargs):
576+ self._logger.log(level, msg, *args, **kwargs)
577+ self._history.log(level, "{__prefmt_msg}",
578+ __prefmt_msg=msg % args, logger=self._logger.name)
579+
580+ def debug(self, msg, *args, **kwargs):
581+ return self.log(logging.DEBUG, msg, *args, **kwargs)
582+
583+ def info(self, msg, *args, **kwargs):
584+ return self.log(logging.INFO, msg, *args, **kwargs)
585+
586+ def warning(self, msg, *args, **kwargs):
587+ return self.log(logging.WARNING, msg, *args, **kwargs)
588+
589+ def error(self, msg, *args, **kwargs):
590+ return self.log(logging.ERROR, msg, *args, **kwargs)
591+
592+ def critical(self, msg, *args, **kwargs):
593+ return self.log(logging.CRITICAL, msg, *args, **kwargs)
594+
595+ def exception(self, msg, *args):
596+ self._logger.exception(msg, *args)
597+
598+
599+class HistoryToLoggerProxy(object):
600+ """
601+ Wrapper that looks like History but also logs to logger
602+ """
603+
604+ _history = global_history
605+
606+ def __init__(self, logger):
607+ self._logger = logger
608+
609+ def log(self, level, message, **meta):
610+ self._logger.log(level, message.format(**meta))
611+ return self._history.log(level, message, **meta)
612+
613+ def debug(self, message, **meta):
614+ return self.log(logging.DEBUG, message, **meta)
615+
616+ def info(self, message, **meta):
617+ return self.log(logging.INFO, message, **meta)
618+
619+ def warning(self, message, **meta):
620+ return self.log(logging.WARNING, message, **meta)
621+
622+ def error(self, message, **meta):
623+ return self.log(logging.ERROR, message, **meta)
624+
625+ def critical(self, message, **meta):
626+ return self.log(logging.CRITICAL, message, **meta)
627+
628+
629+class LoggingMixIn(object):
630+ """
631+ Mix-in that adds a self.logger and self.history instance attributes
632+ """
633+
634+ @property
635+ def _logger(self):
636+ return logging.getLogger(
637+ self.__class__.__module__ + "." + self.__class__.__name__)
638+
639+ @property
640+ def history(self):
641+ """
642+ magic lava.core.history bridge
643+ """
644+ return HistoryToLoggerProxy(self._logger)
645+
646+ @property
647+ def logger(self):
648+ """
649+ magic classic python logging bridge
650+ """
651+ return LoggerToHistoryProxy(self._logger)
652
653=== added directory 'lava/core/tests'
654=== added file 'lava/core/tests/__init__.py'
655=== added file 'lava/core/tests/test_history.py'
656--- lava/core/tests/test_history.py 1970-01-01 00:00:00 +0000
657+++ lava/core/tests/test_history.py 2012-05-16 11:20:24 +0000
658@@ -0,0 +1,278 @@
659+"""
660+lava.core.tests.test_history
661+============================
662+
663+Tests for lava.core.history
664+"""
665+import datetime
666+
667+from unittest2 import TestCase
668+from mocker import Mocker
669+
670+from lava.core.history import Activity, DictProxy, ExceptionInfo, History
671+from lava.core.history import DEBUG, INFO, WARNING, ERROR, CRITICAL
672+from lava.core.history import _build_json
673+
674+
675+class ActivityTestCase(TestCase):
676+
677+ def test_raw_message(self):
678+ act = Activity(INFO, "some {keyword}", keyword='word')
679+ self.assertEqual(act.raw_message, "some {keyword}")
680+
681+ def test_message(self):
682+ act = Activity(INFO, "typical {keyword}", keyword='word')
683+ self.assertEqual(act.message, "typical word")
684+ act = Activity(INFO, "mistake {bad_keyword}")
685+ with self.assertRaises(KeyError):
686+ act.message
687+
688+ def test_level(self):
689+ self.assertEqual(Activity(DEBUG, "").level, DEBUG)
690+ self.assertEqual(Activity(INFO, "").level, INFO)
691+ self.assertEqual(Activity(WARNING, "").level, WARNING)
692+ self.assertEqual(Activity(ERROR, "").level, ERROR)
693+ self.assertEqual(Activity(CRITICAL, "").level, CRITICAL)
694+
695+ def test_level_name(self):
696+ self.assertEqual(Activity(DEBUG, "").level_name, "DEBUG")
697+ self.assertEqual(Activity(INFO, "").level_name, "INFO")
698+ self.assertEqual(Activity(WARNING, "").level_name, "WARNING")
699+ self.assertEqual(Activity(ERROR, "").level_name, "ERROR")
700+ self.assertEqual(Activity(CRITICAL, "").level_name, "CRITICAL")
701+
702+ def test_meta(self):
703+ act = Activity(INFO, "", foo=1)
704+ self.assertEqual(act.meta.foo, 1)
705+
706+ def test_attach(self):
707+ act = Activity(INFO, "")
708+ act.attach("foo")
709+ self.assertEqual(act._attachments, [
710+ DictProxy({'name': 'foo',
711+ 'orig_name': 'foo',
712+ 'mime_type': 'text/plain'})])
713+
714+ def test_attach_uses_basename(self):
715+ act = Activity(INFO, "")
716+ act.attach("/some/place/foo")
717+ self.assertEqual(act._attachments, [
718+ DictProxy({'name': 'foo',
719+ 'orig_name': '/some/place/foo',
720+ 'mime_type': 'text/plain'})])
721+
722+ def test_attach_uses_stream_name(self):
723+ act = Activity(INFO, "")
724+ mocker = Mocker()
725+ stream = mocker.mock()
726+ stream.name
727+ mocker.result("/some/place/foo")
728+ mocker.replay()
729+ act.attach(stream)
730+ self.assertEqual(act._attachments, [
731+ DictProxy({'name': 'foo',
732+ 'orig_name': '/some/place/foo',
733+ 'mime_type': 'text/plain'})])
734+ mocker.verify()
735+
736+ def test_attach_respects_mime_type(self):
737+ act = Activity(INFO, "")
738+ act.attach("foo", mime_type="text/html")
739+ self.assertEqual(act._attachments, [
740+ DictProxy({'name': 'foo',
741+ 'orig_name': 'foo',
742+ 'mime_type': 'text/html'})])
743+
744+ def test_attach_respects_name(self):
745+ act = Activity(INFO, "")
746+ act.attach("foo", name='blarg')
747+ self.assertEqual(act._attachments, [
748+ DictProxy({'name': 'blarg',
749+ 'orig_name': 'foo',
750+ 'mime_type': 'text/plain'})])
751+
752+ def test_duration_for_immediate_activities(self):
753+ act = Activity(INFO, "")
754+ self.assertEqual(act.duration, datetime.timedelta(0))
755+
756+ def test_duration_for_long_activities(self):
757+ act = Activity(INFO, "")
758+ with act:
759+ pass
760+ self.assertNotEqual(act.duration, datetime.timedelta(0))
761+
762+ # TODO: as_json
763+
764+ def test__str__(self):
765+ act = Activity(INFO, "foo {bar}", bar='BAR')
766+ self.assertEqual(str(act), "foo BAR")
767+
768+ # TODO: save_as_tarball
769+
770+ # TODO: _save_tarball_attachments
771+
772+ def test__repr__(self):
773+ act = Activity(INFO, "foo {bar}", bar='BAR')
774+ self.assertEqual(repr(act), "<Activity: 'foo BAR'>")
775+
776+ def test__enter__(self):
777+ act = Activity(INFO, "")
778+ retval = act.__enter__()
779+ self.assertIs(retval, act)
780+ self.assertEqual(act._end, None)
781+
782+ def test__exit__for_normal_termination(self):
783+ act = Activity(INFO, "")
784+ act.__enter__()
785+ act.__exit__(None, None, None)
786+ self.assertNotEqual(act._end, None)
787+ self.assertEqual(act._exc_value, None)
788+ self.assertEqual(act._exc_type, None)
789+ self.assertEqual(act._traceback, None)
790+
791+ def test__exit__for_abnormal_termination(self):
792+ act = Activity(INFO, "")
793+ act.__enter__()
794+ exc = Exception()
795+ tb = [ExceptionInfo('file', 1, 'foo', 'blarg')]
796+ act.__exit__(type(exc), exc, tb)
797+ self.assertNotEqual(act._end, None)
798+ self.assertEqual(act._exc_value, exc)
799+ self.assertEqual(act._exc_type, type(exc))
800+ self.assertEqual(act._traceback, tb)
801+
802+ # TODO: print_
803+
804+ # TODO: _tree_indent
805+
806+
807+class test_build_json(TestCase):
808+
809+ def assertJSON(self, a, b):
810+ self.assertEqual(_build_json(a), b)
811+
812+ def test_dict(self):
813+ self.assertJSON({'foo': 'bar'}, {u'foo': u'bar'})
814+
815+ def test_dict_skips_empty_values(self):
816+ self.assertJSON({'foo': None}, {})
817+ self.assertJSON({'foo': {}}, {})
818+ self.assertJSON({'foo': []}, {})
819+
820+ def test_list_or_tuple(self):
821+ self.assertJSON(['foo'], [u'foo'])
822+ self.assertJSON(('foo', ), [u'foo'])
823+
824+ def test_passthrough(self):
825+ self.assertJSON(1, 1)
826+ self.assertJSON(1.0, 1.0)
827+ self.assertJSON(u"1", u"1")
828+ self.assertJSON(True, True)
829+ self.assertJSON(None, None)
830+
831+ def test_str(self):
832+ self.assertJSON("1", u"1")
833+ with self.assertRaises(UnicodeDecodeError):
834+ self.assertJSON("\xff", u"\xff")
835+
836+ def test_datetime(self):
837+ self.assertJSON(datetime.datetime(2012, 8, 19, 12, 34, 56),
838+ "2012-08-19T12:34:56Z")
839+
840+ def test_timedelta(self):
841+ self.assertJSON(datetime.timedelta(1, 2, 3), "1d 2s 3us")
842+
843+ def test_other(self):
844+ class Foo(object):
845+ def __unicode__(self):
846+ return u"blarg"
847+ self.assertJSON(Foo(), u"blarg")
848+
849+
850+class HistoryTest(TestCase):
851+
852+ def test_top(self):
853+ act = Activity(INFO, "")
854+ hist = History(act)
855+ self.assertIs(hist.top, act)
856+
857+ def test_bottom(self):
858+ act = Activity(INFO, "")
859+ hist = History(act)
860+ self.assertIs(hist.bottom, act)
861+ with hist.info("") as act2:
862+ self.assertIs(hist.bottom, act2)
863+ self.assertIs(hist.bottom, act)
864+
865+ def test_last(self):
866+ act = Activity(INFO, "")
867+ hist = History(act)
868+ self.assertIs(hist.last, act)
869+ with hist.info("") as act2:
870+ pass
871+ self.assertIs(hist.last, act2)
872+
873+ def test_log_appends_to_bottommost_activity(self):
874+ act = Activity(INFO, "")
875+ hist = History(act)
876+ hist.log(INFO, "foo")
877+ self.assertEqual(hist.bottom._sub_activities, [hist.last])
878+
879+ def test_debug(self):
880+ act = Activity(INFO, "")
881+ hist = History(act)
882+ hist.debug("foo")
883+ self.assertEqual(hist.last.level, DEBUG)
884+ self.assertEqual(hist.last.message, "foo")
885+
886+ def test_info(self):
887+ act = Activity(INFO, "")
888+ hist = History(act)
889+ hist.info("foo")
890+ self.assertEqual(hist.last.level, INFO)
891+ self.assertEqual(hist.last.message, "foo")
892+
893+ def test_warning(self):
894+ act = Activity(INFO, "")
895+ hist = History(act)
896+ hist.warning("foo")
897+ self.assertEqual(hist.last.level, WARNING)
898+ self.assertEqual(hist.last.message, "foo")
899+
900+ def test_error(self):
901+ act = Activity(INFO, "")
902+ hist = History(act)
903+ hist.error("foo")
904+ self.assertEqual(hist.last.level, ERROR)
905+ self.assertEqual(hist.last.message, "foo")
906+
907+ def test_critical(self):
908+ act = Activity(INFO, "")
909+ hist = History(act)
910+ hist.critical("foo")
911+ self.assertEqual(hist.last.level, CRITICAL)
912+ self.assertEqual(hist.last.message, "foo")
913+
914+ def test__enter__(self):
915+ act = Activity(INFO, "")
916+ hist = History(act)
917+ ret = hist.__enter__()
918+ # Enter will push the last activity on the nesting stack
919+ self.assertIs(ret, act)
920+ # NOTE: act is now a long activity
921+ self.assertIs(act._end, None)
922+ # NOTE: top of the history is now twice on the nesting stack
923+ self.assertEqual(hist._nesting, [act, act])
924+ hist.info("foo")
925+ self.assertEqual(hist.top._sub_activities[0].message, "foo")
926+
927+ def test__exit__(self):
928+ act = Activity(INFO, "")
929+ hist = History(act)
930+ hist.__enter__()
931+ self.assertEqual(hist._nesting, [act, act])
932+ hist.__exit__(None, None, None)
933+ self.assertEqual(hist._nesting, [act])
934+ # NOTE: act is now a long activity
935+ self.assertIsNot(act._end, None)
936+ self.assertNotEqual(act.duration, datetime.timedelta(0))
937
938=== modified file 'setup.py'
939--- setup.py 2012-05-03 22:42:55 +0000
940+++ setup.py 2012-05-16 11:20:24 +0000
941@@ -44,6 +44,13 @@
942 "Topic :: Software Development :: Testing",
943 ],
944 extras_require={},
945- install_requires=[],
946- tests_require=['unittest2'],
947+ install_requires=[
948+ 'json-document',
949+ 'json-schema-validator',
950+ 'simplejson',
951+ ],
952+ tests_require=[
953+ 'unittest2',
954+ 'mocker',
955+ ],
956 zip_safe=True)

Subscribers

People subscribed via source and target branches