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: 363 lines (+258/-22)
6 files modified
src/sextant/__init__.py (+9/-1)
src/sextant/__main__.py (+41/-17)
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+230938@code.launchpad.net

This proposal supersedes a proposal from 2014-08-14.

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.
Revision history for this message
Patrick Stevens (patrickas) wrote :

I don't see anything particularly wrong with this, although I am not the world's greatest Python programmer!

lp:~pconnell/ensoft-sextant/entrails updated
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.

Revision history for this message
Phil Connell (pconnell) wrote :

Suggestion to use logging rather than print is definitely valid. New request coming...

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

Subscribers

People subscribed via source and target branches

to all changes: