forked from buildbot/buildbot
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Adding-LogObservers.html
351 lines (303 loc) · 15.9 KB
/
Adding-LogObservers.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
<html lang="en">
<head>
<title>Adding LogObservers - BuildBot Manual - 0.8.1beta1</title>
<meta http-equiv="Content-Type" content="text/html">
<meta name="description" content="BuildBot Manual - 0.8.1beta1">
<meta name="generator" content="makeinfo 4.13">
<link title="Top" rel="start" href="index.html#Top">
<link rel="up" href="Writing-New-BuildSteps.html#Writing-New-BuildSteps" title="Writing New BuildSteps">
<link rel="prev" href="Reading-Logfiles.html#Reading-Logfiles" title="Reading Logfiles">
<link rel="next" href="BuildStep-URLs.html#BuildStep-URLs" title="BuildStep URLs">
<link href="http://www.gnu.org/software/texinfo/" rel="generator-home" title="Texinfo Homepage">
<!--
This is the BuildBot manual for Buildbot version 0.8.1beta1.
Copyright (C) 2005, 2006, 2009, 2010 Brian Warner
Copying and distribution of this file, with or without
modification, are permitted in any medium without royalty
provided the copyright notice and this notice are preserved.-->
<meta http-equiv="Content-Style-Type" content="text/css">
<style type="text/css"><!--
pre.display { font-family:inherit }
pre.format { font-family:inherit }
pre.smalldisplay { font-family:inherit; font-size:smaller }
pre.smallformat { font-family:inherit; font-size:smaller }
pre.smallexample { font-size:smaller }
pre.smalllisp { font-size:smaller }
span.sc { font-variant:small-caps }
span.roman { font-family:serif; font-weight:normal; }
span.sansserif { font-family:sans-serif; font-weight:normal; }
--></style>
<!-- GA-TRACKING-START -->
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
try{
var pageTracker = _gat._getTracker("UA-12313843-4");
pageTracker._setDomainName("none");
pageTracker._setAllowLinker(true);
pageTracker._trackPageview();
} catch(err) {}
</script>
<!-- GA-TRACKING-END -->
</head>
<body>
<div class="node">
<a name="Adding-LogObservers"></a>
<p>
Next: <a rel="next" accesskey="n" href="BuildStep-URLs.html#BuildStep-URLs">BuildStep URLs</a>,
Previous: <a rel="previous" accesskey="p" href="Reading-Logfiles.html#Reading-Logfiles">Reading Logfiles</a>,
Up: <a rel="up" accesskey="u" href="Writing-New-BuildSteps.html#Writing-New-BuildSteps">Writing New BuildSteps</a>
<hr>
</div>
<h5 class="subsubsection">4.11.9.4 Adding LogObservers</h5>
<p><a name="index-LogObserver-113"></a><a name="index-LogLineObserver-114"></a>
Most shell commands emit messages to stdout or stderr as they operate,
especially if you ask them nicely with a <code>--verbose</code> flag of some
sort. They may also write text to a log file while they run. Your
BuildStep can watch this output as it arrives, to keep track of how
much progress the command has made. You can get a better measure of
progress by counting the number of source files compiled or test cases
run than by merely tracking the number of bytes that have been written
to stdout. This improves the accuracy and the smoothness of the ETA
display.
<p>To accomplish this, you will need to attach a <code>LogObserver</code> to
one of the log channels, most commonly to the “stdio” channel but
perhaps to another one which tracks a log file. This observer is given
all text as it is emitted from the command, and has the opportunity to
parse that output incrementally. Once the observer has decided that
some event has occurred (like a source file being compiled), it can
use the <code>setProgress</code> method to tell the BuildStep about the
progress that this event represents.
<p>There are a number of pre-built <code>LogObserver</code> classes that you
can choose from (defined in <code>buildbot.process.buildstep</code>, and of
course you can subclass them to add further customization. The
<code>LogLineObserver</code> class handles the grunt work of buffering and
scanning for end-of-line delimiters, allowing your parser to operate
on complete stdout/stderr lines. (Lines longer than a set maximum
length are dropped; the maximum defaults to 16384 bytes, but you can
change it by calling <code>setMaxLineLength()</code> on your
<code>LogLineObserver</code> instance. Use <code>sys.maxint</code> for effective
infinity.)
<p>For example, let's take a look at the <code>TrialTestCaseCounter</code>,
which is used by the Trial step to count test cases as they are run.
As Trial executes, it emits lines like the following:
<pre class="example"> buildbot.test.test_config.ConfigTest.testDebugPassword ... [OK]
buildbot.test.test_config.ConfigTest.testEmpty ... [OK]
buildbot.test.test_config.ConfigTest.testIRC ... [FAIL]
buildbot.test.test_config.ConfigTest.testLocks ... [OK]
</pre>
<p>When the tests are finished, trial emits a long line of “======” and
then some lines which summarize the tests that failed. We want to
avoid parsing these trailing lines, because their format is less
well-defined than the “[OK]” lines.
<p>The parser class looks like this:
<pre class="example"> from buildbot.process.buildstep import LogLineObserver
class TrialTestCaseCounter(LogLineObserver):
_line_re = re.compile(r'^([\w\.]+) \.\.\. \[([^\]]+)\]$')
numTests = 0
finished = False
def outLineReceived(self, line):
if self.finished:
return
if line.startswith("=" * 40):
self.finished = True
return
m = self._line_re.search(line.strip())
if m:
testname, result = m.groups()
self.numTests += 1
self.step.setProgress('tests', self.numTests)
</pre>
<p>This parser only pays attention to stdout, since that's where trial
writes the progress lines. It has a mode flag named <code>finished</code> to
ignore everything after the “====” marker, and a scary-looking
regular expression to match each line while hopefully ignoring other
messages that might get displayed as the test runs.
<p>Each time it identifies a test has been completed, it increments its
counter and delivers the new progress value to the step with
<code>self.step.setProgress</code>. This class is specifically measuring
progress along the “tests” metric, in units of test cases (as
opposed to other kinds of progress like the “output” metric, which
measures in units of bytes). The Progress-tracking code uses each
progress metric separately to come up with an overall completion
percentage and an ETA value.
<p>To connect this parser into the <code>Trial</code> BuildStep,
<code>Trial.__init__</code> ends with the following clause:
<pre class="example"> # this counter will feed Progress along the 'test cases' metric
counter = TrialTestCaseCounter()
self.addLogObserver('stdio', counter)
self.progressMetrics += ('tests',)
</pre>
<p>This creates a TrialTestCaseCounter and tells the step that the
counter wants to watch the “stdio” log. The observer is
automatically given a reference to the step in its <code>.step</code>
attribute.
<h4 class="subheading">A Somewhat Whimsical Example</h4>
<p>Let's say that we've got some snazzy new unit-test framework called
Framboozle. It's the hottest thing since sliced bread. It slices, it
dices, it runs unit tests like there's no tomorrow. Plus if your unit
tests fail, you can use its name for a Web 2.1 startup company, make
millions of dollars, and hire engineers to fix the bugs for you, while
you spend your afternoons lazily hang-gliding along a scenic pacific
beach, blissfully unconcerned about the state of your
tests.<a rel="footnote" href="#fn-1" name="fnd-1"><sup>1</sup></a>
<p>To run a Framboozle-enabled test suite, you just run the 'framboozler'
command from the top of your source code tree. The 'framboozler'
command emits a bunch of stuff to stdout, but the most interesting bit
is that it emits the line "FNURRRGH!" every time it finishes running a
test case<a rel="footnote" href="#fn-2" name="fnd-2"><sup>2</sup></a>. You'd like to have a test-case counting LogObserver that
watches for these lines and counts them, because counting them will
help the buildbot more accurately calculate how long the build will
take, and this will let you know exactly how long you can sneak out of
the office for your hang-gliding lessons without anyone noticing that
you're gone.
<p>This will involve writing a new BuildStep (probably named
"Framboozle") which inherits from ShellCommand. The BuildStep class
definition itself will look something like this:
<pre class="example"> # START
from buildbot.steps.shell import ShellCommand
from buildbot.process.buildstep import LogLineObserver
class FNURRRGHCounter(LogLineObserver):
numTests = 0
def outLineReceived(self, line):
if "FNURRRGH!" in line:
self.numTests += 1
self.step.setProgress('tests', self.numTests)
class Framboozle(ShellCommand):
command = ["framboozler"]
def __init__(self, **kwargs):
ShellCommand.__init__(self, **kwargs) # always upcall!
counter = FNURRRGHCounter())
self.addLogObserver('stdio', counter)
self.progressMetrics += ('tests',)
# FINISH
</pre>
<p>So that's the code that we want to wind up using. How do we actually
deploy it?
<p>You have a couple of different options.
<p>Option 1: The simplest technique is to simply put this text
(everything from START to FINISH) in your master.cfg file, somewhere
before the BuildFactory definition where you actually use it in a
clause like:
<pre class="example"> f = BuildFactory()
f.addStep(SVN(svnurl="stuff"))
f.addStep(Framboozle())
</pre>
<p>Remember that master.cfg is secretly just a python program with one
job: populating the BuildmasterConfig dictionary. And python programs
are allowed to define as many classes as they like. So you can define
classes and use them in the same file, just as long as the class is
defined before some other code tries to use it.
<p>This is easy, and it keeps the point of definition very close to the
point of use, and whoever replaces you after that unfortunate
hang-gliding accident will appreciate being able to easily figure out
what the heck this stupid "Framboozle" step is doing anyways. The
downside is that every time you reload the config file, the Framboozle
class will get redefined, which means that the buildmaster will think
that you've reconfigured all the Builders that use it, even though
nothing changed. Bleh.
<p>Option 2: Instead, we can put this code in a separate file, and import
it into the master.cfg file just like we would the normal buildsteps
like ShellCommand and SVN.
<p>Create a directory named ~/lib/python, put everything from START to
FINISH in ~/lib/python/framboozle.py, and run your buildmaster using:
<pre class="example"> PYTHONPATH=~/lib/python buildbot start MASTERDIR
</pre>
<p>or use the <samp><span class="file">Makefile.buildbot</span></samp> to control the way
<samp><span class="command">buildbot start</span></samp> works. Or add something like this to
something like your ~/.bashrc or ~/.bash_profile or ~/.cshrc:
<pre class="example"> export PYTHONPATH=~/lib/python
</pre>
<p>Once we've done this, our master.cfg can look like:
<pre class="example"> from framboozle import Framboozle
f = BuildFactory()
f.addStep(SVN(svnurl="stuff"))
f.addStep(Framboozle())
</pre>
<p>or:
<pre class="example"> import framboozle
f = BuildFactory()
f.addStep(SVN(svnurl="stuff"))
f.addStep(framboozle.Framboozle())
</pre>
<p>(check out the python docs for details about how "import" and "from A
import B" work).
<p>What we've done here is to tell python that every time it handles an
"import" statement for some named module, it should look in our
~/lib/python/ for that module before it looks anywhere else. After our
directories, it will try in a bunch of standard directories too
(including the one where buildbot is installed). By setting the
PYTHONPATH environment variable, you can add directories to the front
of this search list.
<p>Python knows that once it "import"s a file, it doesn't need to
re-import it again. This means that reconfiguring the buildmaster
(with "buildbot reconfig", for example) won't make it think the
Framboozle class has changed every time, so the Builders that use it
will not be spuriously restarted. On the other hand, you either have
to start your buildmaster in a slightly weird way, or you have to
modify your environment to set the PYTHONPATH variable.
<p>Option 3: Install this code into a standard python library directory
<p>Find out what your python's standard include path is by asking it:
<pre class="example"> 80:warner@luther% python
Python 2.4.4c0 (#2, Oct 2 2006, 00:57:46)
[GCC 4.1.2 20060928 (prerelease) (Debian 4.1.1-15)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> import pprint
>>> pprint.pprint(sys.path)
['',
'/usr/lib/python24.zip',
'/usr/lib/python2.4',
'/usr/lib/python2.4/plat-linux2',
'/usr/lib/python2.4/lib-tk',
'/usr/lib/python2.4/lib-dynload',
'/usr/local/lib/python2.4/site-packages',
'/usr/lib/python2.4/site-packages',
'/usr/lib/python2.4/site-packages/Numeric',
'/var/lib/python-support/python2.4',
'/usr/lib/site-python']
</pre>
<p>In this case, putting the code into
/usr/local/lib/python2.4/site-packages/framboozle.py would work just
fine. We can use the same master.cfg "import framboozle" statement as
in Option 2. By putting it in a standard include directory (instead of
the decidedly non-standard ~/lib/python), we don't even have to set
PYTHONPATH to anything special. The downside is that you probably have
to be root to write to one of those standard include directories.
<p>Option 4: Submit the code for inclusion in the Buildbot distribution
<p>Make a fork of buildbot on http://github.com/djmitche/buildbot or post a patch
in a bug at http://buildbot.net. In either case, post a note about your patch
to the mailing list, so others can provide feedback and, eventually, commit it.
<pre class="example"> from buildbot.steps import framboozle
f = BuildFactory()
f.addStep(SVN(svnurl="stuff"))
f.addStep(framboozle.Framboozle())
</pre>
<p>And then you don't even have to install framboozle.py anywhere on your
system, since it will ship with Buildbot. You don't have to be root,
you don't have to set PYTHONPATH. But you do have to make a good case
for Framboozle being worth going into the main distribution, you'll
probably have to provide docs and some unit test cases, you'll need to
figure out what kind of beer the author likes, and then you'll have to
wait until the next release. But in some environments, all this is
easier than getting root on your buildmaster box, so the tradeoffs may
actually be worth it.
<p>Putting the code in master.cfg (1) makes it available to that
buildmaster instance. Putting it in a file in a personal library
directory (2) makes it available for any buildmasters you might be
running. Putting it in a file in a system-wide shared library
directory (3) makes it available for any buildmasters that anyone on
that system might be running. Getting it into the buildbot's upstream
repository (4) makes it available for any buildmasters that anyone in
the world might be running. It's all a matter of how widely you want
to deploy that new class.
<div class="footnote">
<hr>
<h4>Footnotes</h4><p class="footnote"><small>[<a name="fn-1" href="#fnd-1">1</a>]</small> framboozle.com is still available. Remember, I get 10%
:).</p>
<p class="footnote"><small>[<a name="fn-2" href="#fnd-2">2</a>]</small> Framboozle gets very excited about running unit
tests.</p>
<hr></div>
</body></html>