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

Proposed by Phil Connell
Status: Merged
Approved by: James
Approved revision: 180
Merged at revision: 172
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
Patrick Stevens Approve
Review via email: mp+230957@code.launchpad.net

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

Commit message

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.

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 : Posted in a previous version of this proposal

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

Revision history for this message
Phil Connell (pconnell) wrote : Posted in a previous version of this proposal

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

Revision history for this message
Patrick Stevens (patrickas) wrote :

Looks good to me.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/sextant/__init__.py'
--- src/sextant/__init__.py 2014-08-11 15:14:05 +0000
+++ src/sextant/__init__.py 2014-08-15 10:50:17 +0000
@@ -5,9 +5,17 @@
5# -----------------------------------------5# -----------------------------------------
6"""Program call graph recording and query framework."""6"""Program call graph recording and query framework."""
77
8from . import errors
9from . import pyinput
10
8__all__ = (11__all__ = (
9 "SextantConnection",12 "SextantConnection",
13) + (
14 errors.__all__ +
15 pyinput.__all__
10)16)
1117
12from .update_db import SextantConnection18from .db_api import SextantConnection
19from .errors import *
20from .pyinput import *
1321
1422
=== modified file 'src/sextant/__main__.py'
--- src/sextant/__main__.py 2014-08-14 14:41:19 +0000
+++ src/sextant/__main__.py 2014-08-15 10:50:17 +0000
@@ -5,13 +5,42 @@
5# -----------------------------------------5# -----------------------------------------
6#invokes Sextant and argparse6#invokes Sextant and argparse
77
8from __future__ import absolute_import, print_function
9
8import argparse10import argparse
9import ConfigParser, os, sys11try:
12 import ConfigParser
13except ImportError:
14 import configparser as ConfigParser
15import logging
16import logging.config
17import os
18import sys
1019
11from . import update_db20from . import update_db
12from . import query21from . import query
13from . import db_api22from . import db_api
1423
24
25# @@@ Logging level should be configurable (with some structure to setting up
26# logging).
27logging.config.dictConfig({
28 "version": 1,
29 "handlers": {
30 "console": {
31 "class": "logging.StreamHandler",
32 "level": logging.INFO,
33 "stream": "ext://sys.stderr",
34 },
35 },
36 "root": {
37 "level": logging.DEBUG,
38 "handlers": ["console"],
39 },
40})
41log = logging.getLogger()
42
43
15def get_config_file():44def get_config_file():
16 # get the config file option for neo4j server location and web port number45 # get the config file option for neo4j server location and web port number
17 _ROOT = os.path.abspath(os.path.dirname(__file__))46 _ROOT = os.path.abspath(os.path.dirname(__file__))
@@ -23,23 +52,18 @@
23 example_config = get_data('../etc', 'sextant.conf')52 example_config = get_data('../etc', 'sextant.conf')
2453
25 try:54 try:
26 conffile = (p for p in (home_config, env_config, example_config) if os.path.exists(p)).next()55 conffile = next(p
56 for p in (home_config, env_config, example_config)
57 if os.path.exists(p))
27 except StopIteration:58 except StopIteration:
28 #No config files accessable59 #No config files accessable
29 if os.environ.has_key("SEXTANT_CONFIG"):60 if "SEXTANT_CONFIG" in os.environ:
30 #SEXTANT_CONFIG environment variable is set61 #SEXTANT_CONFIG environment variable is set
31 print("SEXTANT_CONFIG currently {}, is set incorrectly.".format(env_config))62 log.error("SEXTANT_CONFIG file %r doesn't exist.", env_config)
32 print("Sextant requires a configuration file.\n")63 log.error("Sextant requires a configuration file.")
33 sys.exit(1)64 sys.exit(1)
3465
35 if conffile is example_config and os.environ.has_key("SEXTANT_CONFIG"):66 log.info("Sextant is using config file %s", conffile)
36 #example config file is available but the user has set the
37 # environment variable, therefore we assume the user actually
38 # wants to use a different config file.
39 print("SEXTANT_CONFIG currently {}, is set incorrectly.".format(env_config))
40 sys.exit(1)
41
42 print("Sextant using config file{}".format(conffile))
43 return conffile67 return conffile
4468
45conffile = get_config_file()69conffile = get_config_file()
@@ -129,8 +153,8 @@
129def _start_web(args):153def _start_web(args):
130 # Don't import at top level -- this makes twisted dependency semi-optional,154 # Don't import at top level -- this makes twisted dependency semi-optional,
131 # allowing non-web functionality to work with Python 3.155 # allowing non-web functionality to work with Python 3.
132 from web import server156 from .web import server
133 print("Serving site on port {}".format(args.port))157 log.info("Serving site on port %s", args.port)
134 server.serve_site(input_database_url=args.remote_neo4j, port=args.port)158 server.serve_site(input_database_url=args.remote_neo4j, port=args.port)
135159
136parsers['web'].set_defaults(func=_start_web)160parsers['web'].set_defaults(func=_start_web)
137161
=== modified file 'src/sextant/db_api.py'
--- src/sextant/db_api.py 2014-08-14 13:53:59 +0000
+++ src/sextant/db_api.py 2014-08-15 10:50:17 +0000
@@ -375,14 +375,15 @@
375 The name specified must pass Validator.validate()ion; this is a measure375 The name specified must pass Validator.validate()ion; this is a measure
376 to prevent Cypher injection attacks.376 to prevent Cypher injection attacks.
377 :param name_of_program: string program name377 :param name_of_program: string program name
378 :return: False if unsuccessful, or an AddToDatabase if successful378 :return: AddToDatabase instance if successful
379 """379 """
380380
381 if not Validator.validate(name_of_program):381 if not Validator.validate(name_of_program):
382 return False382 raise ValueError(
383 "{} is not a valid program name".format(name_of_program))
383384
384 return AddToDatabase(sextant_connection=self,385 return AddToDatabase(sextant_connection=self,
385 program_name=name_of_program) or False386 program_name=name_of_program)
386387
387 def delete_program(self, name_of_program):388 def delete_program(self, name_of_program):
388 """389 """
389390
=== added file 'src/sextant/errors.py'
--- src/sextant/errors.py 1970-01-01 00:00:00 +0000
+++ src/sextant/errors.py 2014-08-15 10:50:17 +0000
@@ -0,0 +1,20 @@
1# -----------------------------------------------------------------------------
2# errors.py -- Sextant error definitions
3#
4# August 2014, Phil Connell
5#
6# Copyright 2014, Ensoft Ltd.
7# -----------------------------------------------------------------------------
8
9from __future__ import absolute_import, print_function
10
11__all__ = (
12 "MissingDependencyError",
13)
14
15
16class MissingDependencyError(Exception):
17 """
18 Raised if trying an operation for which an optional dependency is missing.
19 """
20
021
=== added file 'src/sextant/pyinput.py'
--- src/sextant/pyinput.py 1970-01-01 00:00:00 +0000
+++ src/sextant/pyinput.py 2014-08-15 10:50:17 +0000
@@ -0,0 +1,180 @@
1# -----------------------------------------------------------------------------
2# pyinput.py -- Input information from Python programs.
3#
4# August 2014, Phil Connell
5#
6# Copyright 2014, Ensoft Ltd.
7# -----------------------------------------------------------------------------
8
9from __future__ import absolute_import, print_function
10
11__all__ = (
12 "trace",
13)
14
15
16import contextlib
17import sys
18
19from . import errors
20from . import db_api
21
22
23# Optional, should be checked at API entrypoints requiring entrails (and
24# yes, the handling is a bit fugly).
25try:
26 import entrails
27except ImportError:
28 _entrails_available = False
29 class entrails:
30 EntrailsOutput = object
31else:
32 _entrails_available = True
33
34
35class _SextantOutput(entrails.EntrailsOutput):
36 """Record calls traced by entrails in a sextant database."""
37
38 # Internal attributes:
39 #
40 # _conn:
41 # Sextant connection.
42 # _fns:
43 # Stack of function names (implemented as a list), reflecting the current
44 # call stack, based on enter, exception and exit events.
45 # _prog:
46 # Sextant program representation.
47 _conn = None
48 _fns = None
49 _prog = None
50
51 def __init__(self, conn, program_name):
52 """
53 Initialise this output.
54
55 conn:
56 Connection to the Sextant database.
57 program_name:
58 String used to refer to the traced program in sextant.
59
60 """
61 self._conn = conn
62 self._fns = []
63 self._prog = self._conn.new_program(program_name)
64 self._tracer = self._trace()
65 next(self._tracer)
66
67 def _add_frame(self, event):
68 """Add a function call to the internal stack."""
69 name = event.qualname()
70 self._fns.append(name)
71 self._prog.add_function(name)
72
73 try:
74 prev_name = self._fns[-2]
75 except IndexError:
76 pass
77 else:
78 self._prog.add_function_call(prev_name, name)
79
80 def _remove_frame(self, event):
81 """Remove a function call from the internal stack."""
82 assert event.qualname() == self._fns[-1], \
83 "Unexpected event for {}".format(event.qualname())
84 self._fns.pop()
85
86 def _handle_simple_event(self, what, event):
87 """Handle a single trace event, not needing recursive processing."""
88 handled = True
89
90 if what == "enter":
91 self._add_frame(event)
92 elif what == "exit":
93 self._remove_frame(event)
94 else:
95 handled = False
96
97 return handled
98
99 def _trace(self):
100 """Coroutine that processes trace events it's sent."""
101 while True:
102 what, event = yield
103
104 handled = self._handle_simple_event(what, event)
105 if not handled:
106 if what == "exception":
107 # An exception doesn't necessarily mean the current stack
108 # frame is exiting. Need to check whether the next event is
109 # an exception in a different stack frame, implying that
110 # the exception is propagating up the stack.
111 while True:
112 prev_event = event
113 prev_name = event.qualname()
114 what, event = yield
115 if event == "exception":
116 if event.qualname() != prev_name:
117 self._remove_frame(prev_event)
118 else:
119 handled = self._handle_simple_event(what, event)
120 assert handled
121 break
122
123 else:
124 raise NotImplementedError
125
126 def close(self):
127 self._prog.commit()
128
129 def enter(self, event):
130 self._tracer.send(("enter", event))
131
132 def exception(self, event):
133 self._tracer.send(("exception", event))
134
135 def exit(self, event):
136 self._tracer.send(("exit", event))
137
138
139# @@@ config parsing shouldn't be done in __main__ (we want to get the neo4j
140# url from there...)
141@contextlib.contextmanager
142def trace(conn, program_name=None, filters=None):
143 """
144 Context manager that records function calls in its context block.
145
146 e.g. given this code:
147
148 with sextant.trace("http://localhost:7474"):
149 foo()
150 bar()
151
152 The calls to foo() and bar() (and their callees, at any depth) will be
153 recorded in the sextant database.
154
155 conn:
156 Instance of SextantConnection that will be used to record calls.
157 program_name:
158 String used to refer to the traced program in sextant. Defaults to
159 sys.argv[0].
160 filters:
161 Optional iterable of entrails filters to apply.
162
163 """
164 if not _entrails_available:
165 raise errors.MissingDependencyError(
166 "Entrails is required to trace execution")
167
168 if program_name is None:
169 program_name = sys.argv[0]
170
171 tracer = entrails.Entrails(filters=filters)
172 tracer.add_output(_SextantOutput(conn, program_name))
173
174 tracer.start_trace()
175 try:
176 yield
177 finally:
178 # Flush traced data.
179 tracer.end_trace()
180
0181
=== modified file 'src/sextant/web/server.py'
--- src/sextant/web/server.py 2014-08-14 09:03:17 +0000
+++ src/sextant/web/server.py 2014-08-15 10:50:17 +0000
@@ -301,7 +301,10 @@
301301
302 global database_url302 global database_url
303 database_url = input_database_url303 database_url = input_database_url
304 root = File(os.path.dirname(os.path.abspath(__file__))+'/../../resources/web/') # serve static directory at root304 # serve static directory at root
305 root = File(os.path.join(
306 os.path.dirname(os.path.abspath(__file__)),
307 "..", "..", "..", "resources", "web"))
305308
306 # serve a dynamic Echoer webpage at /echoer.html309 # serve a dynamic Echoer webpage at /echoer.html
307 root.putChild("echoer.html", Echoer())310 root.putChild("echoer.html", Echoer())

Subscribers

People subscribed via source and target branches

to all changes: