Merge lp:~pconnell/ensoft-sextant/entrails into lp:~ensoft-opensource/ensoft-sextant/trunk

Proposed by Phil Connell
Status: Superseded
Proposed branch: lp:~pconnell/ensoft-sextant/entrails
Merge into: lp:~ensoft-opensource/ensoft-sextant/trunk
Diff against target: 335 lines (+236/-13)
6 files modified
src/sextant/__init__.py (+9/-1)
src/sextant/__main__.py (+19/-8)
src/sextant/db_api.py (+4/-3)
src/sextant/errors.py (+20/-0)
src/sextant/pyinput.py (+180/-0)
src/sextant/web/server.py (+4/-1)
To merge this branch: bzr merge lp:~pconnell/ensoft-sextant/entrails
Reviewer Review Type Date Requested Status
Ensoft Open Source Pending
Review via email: mp+230867@code.launchpad.net

This proposal has been superseded by a proposal from 2014-08-15.

Description of the change

Integrate with entrails to enable runtime tracing of Python programs.

Upshot: it's now possible to get callgraphs from Python programs into Sextant.

Specifically, a new context manager is added, sextant.trace(). Any calls inside the with block are traced.

There are also a few minor bugfixes (mostly related to Python 3 support). Commit messages have some details.

To post a comment you must log in.
lp:~pconnell/ensoft-sextant/entrails updated
176. By Phil Connell <email address hidden>

sextant.trace() should take a connection, not a DB URL

177. By Phil Connell <email address hidden>

new_program() should raise on errors, not return a different type

178. By Phil Connell <email address hidden>

Correct import of web package (explicitly relative rather than implicit)

179. By Phil Connell <email address hidden>

Add missing import of names from errors module

180. By Phil Connell <email address hidden>

Using logging, not print in sextant.__main__

Setup of logging is a bit hacked in, something to clean up later.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/sextant/__init__.py'
2--- src/sextant/__init__.py 2014-08-11 15:14:05 +0000
3+++ src/sextant/__init__.py 2014-08-15 08:43:22 +0000
4@@ -5,9 +5,17 @@
5 # -----------------------------------------
6 """Program call graph recording and query framework."""
7
8+from . import errors
9+from . import pyinput
10+
11 __all__ = (
12 "SextantConnection",
13+) + (
14+ errors.__all__ +
15+ pyinput.__all__
16 )
17
18-from .update_db import SextantConnection
19+from .db_api import SextantConnection
20+from .errors import *
21+from .pyinput import *
22
23
24=== modified file 'src/sextant/__main__.py'
25--- src/sextant/__main__.py 2014-08-14 14:41:19 +0000
26+++ src/sextant/__main__.py 2014-08-15 08:43:22 +0000
27@@ -5,8 +5,15 @@
28 # -----------------------------------------
29 #invokes Sextant and argparse
30
31+from __future__ import absolute_import, print_function
32+
33 import argparse
34-import ConfigParser, os, sys
35+try:
36+ import ConfigParser
37+except ImportError:
38+ import configparser as ConfigParser
39+import os
40+import sys
41
42 from . import update_db
43 from . import query
44@@ -23,23 +30,27 @@
45 example_config = get_data('../etc', 'sextant.conf')
46
47 try:
48- conffile = (p for p in (home_config, env_config, example_config) if os.path.exists(p)).next()
49+ conffile = next(p
50+ for p in (home_config, env_config, example_config)
51+ if os.path.exists(p))
52 except StopIteration:
53 #No config files accessable
54- if os.environ.has_key("SEXTANT_CONFIG"):
55+ if "SEXTANT_CONFIG" in os.environ:
56 #SEXTANT_CONFIG environment variable is set
57- print("SEXTANT_CONFIG currently {}, is set incorrectly.".format(env_config))
58- print("Sextant requires a configuration file.\n")
59+ print("SEXTANT_CONFIG currently {}, is set "
60+ "incorrectly.".format(env_config), file=sys.stderr)
61+ print("Sextant requires a configuration file.\n", file=sys.stderr)
62 sys.exit(1)
63
64 if conffile is example_config and os.environ.has_key("SEXTANT_CONFIG"):
65 #example config file is available but the user has set the
66 # environment variable, therefore we assume the user actually
67 # wants to use a different config file.
68- print("SEXTANT_CONFIG currently {}, is set incorrectly.".format(env_config))
69+ print("SEXTANT_CONFIG currently {}, is set "
70+ "incorrectly.".format(env_config), file=sys.stderr)
71 sys.exit(1)
72
73- print("Sextant using config file{}".format(conffile))
74+ print("Sextant using config file{}".format(conffile), file=sys.stderr)
75 return conffile
76
77 conffile = get_config_file()
78@@ -129,7 +140,7 @@
79 def _start_web(args):
80 # Don't import at top level -- this makes twisted dependency semi-optional,
81 # allowing non-web functionality to work with Python 3.
82- from web import server
83+ from .web import server
84 print("Serving site on port {}".format(args.port))
85 server.serve_site(input_database_url=args.remote_neo4j, port=args.port)
86
87
88=== modified file 'src/sextant/db_api.py'
89--- src/sextant/db_api.py 2014-08-14 13:53:59 +0000
90+++ src/sextant/db_api.py 2014-08-15 08:43:22 +0000
91@@ -375,14 +375,15 @@
92 The name specified must pass Validator.validate()ion; this is a measure
93 to prevent Cypher injection attacks.
94 :param name_of_program: string program name
95- :return: False if unsuccessful, or an AddToDatabase if successful
96+ :return: AddToDatabase instance if successful
97 """
98
99 if not Validator.validate(name_of_program):
100- return False
101+ raise ValueError(
102+ "{} is not a valid program name".format(name_of_program))
103
104 return AddToDatabase(sextant_connection=self,
105- program_name=name_of_program) or False
106+ program_name=name_of_program)
107
108 def delete_program(self, name_of_program):
109 """
110
111=== added file 'src/sextant/errors.py'
112--- src/sextant/errors.py 1970-01-01 00:00:00 +0000
113+++ src/sextant/errors.py 2014-08-15 08:43:22 +0000
114@@ -0,0 +1,20 @@
115+# -----------------------------------------------------------------------------
116+# errors.py -- Sextant error definitions
117+#
118+# August 2014, Phil Connell
119+#
120+# Copyright 2014, Ensoft Ltd.
121+# -----------------------------------------------------------------------------
122+
123+from __future__ import absolute_import, print_function
124+
125+__all__ = (
126+ "MissingDependencyError",
127+)
128+
129+
130+class MissingDependencyError(Exception):
131+ """
132+ Raised if trying an operation for which an optional dependency is missing.
133+ """
134+
135
136=== added file 'src/sextant/pyinput.py'
137--- src/sextant/pyinput.py 1970-01-01 00:00:00 +0000
138+++ src/sextant/pyinput.py 2014-08-15 08:43:22 +0000
139@@ -0,0 +1,180 @@
140+# -----------------------------------------------------------------------------
141+# pyinput.py -- Input information from Python programs.
142+#
143+# August 2014, Phil Connell
144+#
145+# Copyright 2014, Ensoft Ltd.
146+# -----------------------------------------------------------------------------
147+
148+from __future__ import absolute_import, print_function
149+
150+__all__ = (
151+ "trace",
152+)
153+
154+
155+import contextlib
156+import sys
157+
158+from . import errors
159+from . import db_api
160+
161+
162+# Optional, should be checked at API entrypoints requiring entrails (and
163+# yes, the handling is a bit fugly).
164+try:
165+ import entrails
166+except ImportError:
167+ _entrails_available = False
168+ class entrails:
169+ EntrailsOutput = object
170+else:
171+ _entrails_available = True
172+
173+
174+class _SextantOutput(entrails.EntrailsOutput):
175+ """Record calls traced by entrails in a sextant database."""
176+
177+ # Internal attributes:
178+ #
179+ # _conn:
180+ # Sextant connection.
181+ # _fns:
182+ # Stack of function names (implemented as a list), reflecting the current
183+ # call stack, based on enter, exception and exit events.
184+ # _prog:
185+ # Sextant program representation.
186+ _conn = None
187+ _fns = None
188+ _prog = None
189+
190+ def __init__(self, conn, program_name):
191+ """
192+ Initialise this output.
193+
194+ conn:
195+ Connection to the Sextant database.
196+ program_name:
197+ String used to refer to the traced program in sextant.
198+
199+ """
200+ self._conn = conn
201+ self._fns = []
202+ self._prog = self._conn.new_program(program_name)
203+ self._tracer = self._trace()
204+ next(self._tracer)
205+
206+ def _add_frame(self, event):
207+ """Add a function call to the internal stack."""
208+ name = event.qualname()
209+ self._fns.append(name)
210+ self._prog.add_function(name)
211+
212+ try:
213+ prev_name = self._fns[-2]
214+ except IndexError:
215+ pass
216+ else:
217+ self._prog.add_function_call(prev_name, name)
218+
219+ def _remove_frame(self, event):
220+ """Remove a function call from the internal stack."""
221+ assert event.qualname() == self._fns[-1], \
222+ "Unexpected event for {}".format(event.qualname())
223+ self._fns.pop()
224+
225+ def _handle_simple_event(self, what, event):
226+ """Handle a single trace event, not needing recursive processing."""
227+ handled = True
228+
229+ if what == "enter":
230+ self._add_frame(event)
231+ elif what == "exit":
232+ self._remove_frame(event)
233+ else:
234+ handled = False
235+
236+ return handled
237+
238+ def _trace(self):
239+ """Coroutine that processes trace events it's sent."""
240+ while True:
241+ what, event = yield
242+
243+ handled = self._handle_simple_event(what, event)
244+ if not handled:
245+ if what == "exception":
246+ # An exception doesn't necessarily mean the current stack
247+ # frame is exiting. Need to check whether the next event is
248+ # an exception in a different stack frame, implying that
249+ # the exception is propagating up the stack.
250+ while True:
251+ prev_event = event
252+ prev_name = event.qualname()
253+ what, event = yield
254+ if event == "exception":
255+ if event.qualname() != prev_name:
256+ self._remove_frame(prev_event)
257+ else:
258+ handled = self._handle_simple_event(what, event)
259+ assert handled
260+ break
261+
262+ else:
263+ raise NotImplementedError
264+
265+ def close(self):
266+ self._prog.commit()
267+
268+ def enter(self, event):
269+ self._tracer.send(("enter", event))
270+
271+ def exception(self, event):
272+ self._tracer.send(("exception", event))
273+
274+ def exit(self, event):
275+ self._tracer.send(("exit", event))
276+
277+
278+# @@@ config parsing shouldn't be done in __main__ (we want to get the neo4j
279+# url from there...)
280+@contextlib.contextmanager
281+def trace(conn, program_name=None, filters=None):
282+ """
283+ Context manager that records function calls in its context block.
284+
285+ e.g. given this code:
286+
287+ with sextant.trace("http://localhost:7474"):
288+ foo()
289+ bar()
290+
291+ The calls to foo() and bar() (and their callees, at any depth) will be
292+ recorded in the sextant database.
293+
294+ conn:
295+ Instance of SextantConnection that will be used to record calls.
296+ program_name:
297+ String used to refer to the traced program in sextant. Defaults to
298+ sys.argv[0].
299+ filters:
300+ Optional iterable of entrails filters to apply.
301+
302+ """
303+ if not _entrails_available:
304+ raise errors.MissingDependencyError(
305+ "Entrails is required to trace execution")
306+
307+ if program_name is None:
308+ program_name = sys.argv[0]
309+
310+ tracer = entrails.Entrails(filters=filters)
311+ tracer.add_output(_SextantOutput(conn, program_name))
312+
313+ tracer.start_trace()
314+ try:
315+ yield
316+ finally:
317+ # Flush traced data.
318+ tracer.end_trace()
319+
320
321=== modified file 'src/sextant/web/server.py'
322--- src/sextant/web/server.py 2014-08-14 09:03:17 +0000
323+++ src/sextant/web/server.py 2014-08-15 08:43:22 +0000
324@@ -301,7 +301,10 @@
325
326 global database_url
327 database_url = input_database_url
328- root = File(os.path.dirname(os.path.abspath(__file__))+'/../../resources/web/') # serve static directory at root
329+ # serve static directory at root
330+ root = File(os.path.join(
331+ os.path.dirname(os.path.abspath(__file__)),
332+ "..", "..", "..", "resources", "web"))
333
334 # serve a dynamic Echoer webpage at /echoer.html
335 root.putChild("echoer.html", Echoer())

Subscribers

People subscribed via source and target branches

to all changes: