├── .gitignore
├── LICENSE
├── README.md
├── Rakefile
├── bin
└── pencil
├── docs
└── pencil_options.md
├── examples
├── dashboards.yml
├── graphs.yml
└── pencil.yml
├── lib
├── pencil.rb
└── pencil
│ ├── config.rb
│ ├── config.ru
│ ├── helpers.rb
│ ├── models.rb
│ ├── models
│ ├── base.rb
│ ├── dashboard.rb
│ ├── graph.rb
│ └── host.rb
│ ├── public
│ ├── css
│ │ └── jquery_themes
│ │ │ └── pencil
│ │ │ ├── images
│ │ │ ├── ui-bg_flat_30_cccccc_40x100.png
│ │ │ ├── ui-bg_flat_50_5c5c5c_40x100.png
│ │ │ ├── ui-bg_glass_20_333333_1x400.png
│ │ │ ├── ui-bg_glass_40_000000_1x400.png
│ │ │ ├── ui-bg_glass_40_ffc73d_1x400.png
│ │ │ ├── ui-bg_gloss-wave_25_333333_500x100.png
│ │ │ ├── ui-bg_highlight-soft_15_000000_1x100.png
│ │ │ ├── ui-bg_highlight-soft_80_eeeeee_1x100.png
│ │ │ ├── ui-bg_inset-soft_30_ff4e0a_1x100.png
│ │ │ ├── ui-icons_222222_256x240.png
│ │ │ ├── ui-icons_4b8e0b_256x240.png
│ │ │ ├── ui-icons_a83300_256x240.png
│ │ │ ├── ui-icons_cccccc_256x240.png
│ │ │ └── ui-icons_ffffff_256x240.png
│ │ │ └── jquery-ui-1.8.16.custom.css
│ ├── favicon.ico
│ ├── js
│ │ ├── cufon.js
│ │ ├── gotham.font.js
│ │ ├── jquery-1.6.2.min.js
│ │ ├── jquery-ui-1.8.16.custom.min.js
│ │ ├── pencil.js
│ │ └── product-design.font.js
│ └── style.css
│ ├── rubyfixes.rb
│ ├── version.rb
│ └── views
│ ├── cluster.erb
│ ├── dash-cluster-zoom.erb
│ ├── dash-cluster.erb
│ ├── dash-global-zoom.erb
│ ├── dash-global.erb
│ ├── global.erb
│ ├── host.erb
│ ├── layout.erb
│ └── partials
│ ├── cluster_selector.erb
│ ├── cluster_switcher.erb
│ ├── dash_switcher.erb
│ ├── graph_switcher.erb
│ ├── hosts_selector.erb
│ ├── input_boxes.erb
│ └── shortcuts.erb
└── pencil.gemspec
/.gitignore:
--------------------------------------------------------------------------------
1 | .dired
2 | local
3 | *.gem
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MOZILLA PUBLIC LICENSE
2 | Version 1.1
3 |
4 | ---------------
5 |
6 | 1. Definitions.
7 |
8 | 1.0.1. "Commercial Use" means distribution or otherwise making the
9 | Covered Code available to a third party.
10 |
11 | 1.1. "Contributor" means each entity that creates or contributes to
12 | the creation of Modifications.
13 |
14 | 1.2. "Contributor Version" means the combination of the Original
15 | Code, prior Modifications used by a Contributor, and the Modifications
16 | made by that particular Contributor.
17 |
18 | 1.3. "Covered Code" means the Original Code or Modifications or the
19 | combination of the Original Code and Modifications, in each case
20 | including portions thereof.
21 |
22 | 1.4. "Electronic Distribution Mechanism" means a mechanism generally
23 | accepted in the software development community for the electronic
24 | transfer of data.
25 |
26 | 1.5. "Executable" means Covered Code in any form other than Source
27 | Code.
28 |
29 | 1.6. "Initial Developer" means the individual or entity identified
30 | as the Initial Developer in the Source Code notice required by Exhibit
31 | A.
32 |
33 | 1.7. "Larger Work" means a work which combines Covered Code or
34 | portions thereof with code not governed by the terms of this License.
35 |
36 | 1.8. "License" means this document.
37 |
38 | 1.8.1. "Licensable" means having the right to grant, to the maximum
39 | extent possible, whether at the time of the initial grant or
40 | subsequently acquired, any and all of the rights conveyed herein.
41 |
42 | 1.9. "Modifications" means any addition to or deletion from the
43 | substance or structure of either the Original Code or any previous
44 | Modifications. When Covered Code is released as a series of files, a
45 | Modification is:
46 | A. Any addition to or deletion from the contents of a file
47 | containing Original Code or previous Modifications.
48 |
49 | B. Any new file that contains any part of the Original Code or
50 | previous Modifications.
51 |
52 | 1.10. "Original Code" means Source Code of computer software code
53 | which is described in the Source Code notice required by Exhibit A as
54 | Original Code, and which, at the time of its release under this
55 | License is not already Covered Code governed by this License.
56 |
57 | 1.10.1. "Patent Claims" means any patent claim(s), now owned or
58 | hereafter acquired, including without limitation, method, process,
59 | and apparatus claims, in any patent Licensable by grantor.
60 |
61 | 1.11. "Source Code" means the preferred form of the Covered Code for
62 | making modifications to it, including all modules it contains, plus
63 | any associated interface definition files, scripts used to control
64 | compilation and installation of an Executable, or source code
65 | differential comparisons against either the Original Code or another
66 | well known, available Covered Code of the Contributor's choice. The
67 | Source Code can be in a compressed or archival form, provided the
68 | appropriate decompression or de-archiving software is widely available
69 | for no charge.
70 |
71 | 1.12. "You" (or "Your") means an individual or a legal entity
72 | exercising rights under, and complying with all of the terms of, this
73 | License or a future version of this License issued under Section 6.1.
74 | For legal entities, "You" includes any entity which controls, is
75 | controlled by, or is under common control with You. For purposes of
76 | this definition, "control" means (a) the power, direct or indirect,
77 | to cause the direction or management of such entity, whether by
78 | contract or otherwise, or (b) ownership of more than fifty percent
79 | (50%) of the outstanding shares or beneficial ownership of such
80 | entity.
81 |
82 | 2. Source Code License.
83 |
84 | 2.1. The Initial Developer Grant.
85 | The Initial Developer hereby grants You a world-wide, royalty-free,
86 | non-exclusive license, subject to third party intellectual property
87 | claims:
88 | (a) under intellectual property rights (other than patent or
89 | trademark) Licensable by Initial Developer to use, reproduce,
90 | modify, display, perform, sublicense and distribute the Original
91 | Code (or portions thereof) with or without Modifications, and/or
92 | as part of a Larger Work; and
93 |
94 | (b) under Patents Claims infringed by the making, using or
95 | selling of Original Code, to make, have made, use, practice,
96 | sell, and offer for sale, and/or otherwise dispose of the
97 | Original Code (or portions thereof).
98 |
99 | (c) the licenses granted in this Section 2.1(a) and (b) are
100 | effective on the date Initial Developer first distributes
101 | Original Code under the terms of this License.
102 |
103 | (d) Notwithstanding Section 2.1(b) above, no patent license is
104 | granted: 1) for code that You delete from the Original Code; 2)
105 | separate from the Original Code; or 3) for infringements caused
106 | by: i) the modification of the Original Code or ii) the
107 | combination of the Original Code with other software or devices.
108 |
109 | 2.2. Contributor Grant.
110 | Subject to third party intellectual property claims, each Contributor
111 | hereby grants You a world-wide, royalty-free, non-exclusive license
112 |
113 | (a) under intellectual property rights (other than patent or
114 | trademark) Licensable by Contributor, to use, reproduce, modify,
115 | display, perform, sublicense and distribute the Modifications
116 | created by such Contributor (or portions thereof) either on an
117 | unmodified basis, with other Modifications, as Covered Code
118 | and/or as part of a Larger Work; and
119 |
120 | (b) under Patent Claims infringed by the making, using, or
121 | selling of Modifications made by that Contributor either alone
122 | and/or in combination with its Contributor Version (or portions
123 | of such combination), to make, use, sell, offer for sale, have
124 | made, and/or otherwise dispose of: 1) Modifications made by that
125 | Contributor (or portions thereof); and 2) the combination of
126 | Modifications made by that Contributor with its Contributor
127 | Version (or portions of such combination).
128 |
129 | (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
130 | effective on the date Contributor first makes Commercial Use of
131 | the Covered Code.
132 |
133 | (d) Notwithstanding Section 2.2(b) above, no patent license is
134 | granted: 1) for any code that Contributor has deleted from the
135 | Contributor Version; 2) separate from the Contributor Version;
136 | 3) for infringements caused by: i) third party modifications of
137 | Contributor Version or ii) the combination of Modifications made
138 | by that Contributor with other software (except as part of the
139 | Contributor Version) or other devices; or 4) under Patent Claims
140 | infringed by Covered Code in the absence of Modifications made by
141 | that Contributor.
142 |
143 | 3. Distribution Obligations.
144 |
145 | 3.1. Application of License.
146 | The Modifications which You create or to which You contribute are
147 | governed by the terms of this License, including without limitation
148 | Section 2.2. The Source Code version of Covered Code may be
149 | distributed only under the terms of this License or a future version
150 | of this License released under Section 6.1, and You must include a
151 | copy of this License with every copy of the Source Code You
152 | distribute. You may not offer or impose any terms on any Source Code
153 | version that alters or restricts the applicable version of this
154 | License or the recipients' rights hereunder. However, You may include
155 | an additional document offering the additional rights described in
156 | Section 3.5.
157 |
158 | 3.2. Availability of Source Code.
159 | Any Modification which You create or to which You contribute must be
160 | made available in Source Code form under the terms of this License
161 | either on the same media as an Executable version or via an accepted
162 | Electronic Distribution Mechanism to anyone to whom you made an
163 | Executable version available; and if made available via Electronic
164 | Distribution Mechanism, must remain available for at least twelve (12)
165 | months after the date it initially became available, or at least six
166 | (6) months after a subsequent version of that particular Modification
167 | has been made available to such recipients. You are responsible for
168 | ensuring that the Source Code version remains available even if the
169 | Electronic Distribution Mechanism is maintained by a third party.
170 |
171 | 3.3. Description of Modifications.
172 | You must cause all Covered Code to which You contribute to contain a
173 | file documenting the changes You made to create that Covered Code and
174 | the date of any change. You must include a prominent statement that
175 | the Modification is derived, directly or indirectly, from Original
176 | Code provided by the Initial Developer and including the name of the
177 | Initial Developer in (a) the Source Code, and (b) in any notice in an
178 | Executable version or related documentation in which You describe the
179 | origin or ownership of the Covered Code.
180 |
181 | 3.4. Intellectual Property Matters
182 | (a) Third Party Claims.
183 | If Contributor has knowledge that a license under a third party's
184 | intellectual property rights is required to exercise the rights
185 | granted by such Contributor under Sections 2.1 or 2.2,
186 | Contributor must include a text file with the Source Code
187 | distribution titled "LEGAL" which describes the claim and the
188 | party making the claim in sufficient detail that a recipient will
189 | know whom to contact. If Contributor obtains such knowledge after
190 | the Modification is made available as described in Section 3.2,
191 | Contributor shall promptly modify the LEGAL file in all copies
192 | Contributor makes available thereafter and shall take other steps
193 | (such as notifying appropriate mailing lists or newsgroups)
194 | reasonably calculated to inform those who received the Covered
195 | Code that new knowledge has been obtained.
196 |
197 | (b) Contributor APIs.
198 | If Contributor's Modifications include an application programming
199 | interface and Contributor has knowledge of patent licenses which
200 | are reasonably necessary to implement that API, Contributor must
201 | also include this information in the LEGAL file.
202 |
203 | (c) Representations.
204 | Contributor represents that, except as disclosed pursuant to
205 | Section 3.4(a) above, Contributor believes that Contributor's
206 | Modifications are Contributor's original creation(s) and/or
207 | Contributor has sufficient rights to grant the rights conveyed by
208 | this License.
209 |
210 | 3.5. Required Notices.
211 | You must duplicate the notice in Exhibit A in each file of the Source
212 | Code. If it is not possible to put such notice in a particular Source
213 | Code file due to its structure, then You must include such notice in a
214 | location (such as a relevant directory) where a user would be likely
215 | to look for such a notice. If You created one or more Modification(s)
216 | You may add your name as a Contributor to the notice described in
217 | Exhibit A. You must also duplicate this License in any documentation
218 | for the Source Code where You describe recipients' rights or ownership
219 | rights relating to Covered Code. You may choose to offer, and to
220 | charge a fee for, warranty, support, indemnity or liability
221 | obligations to one or more recipients of Covered Code. However, You
222 | may do so only on Your own behalf, and not on behalf of the Initial
223 | Developer or any Contributor. You must make it absolutely clear than
224 | any such warranty, support, indemnity or liability obligation is
225 | offered by You alone, and You hereby agree to indemnify the Initial
226 | Developer and every Contributor for any liability incurred by the
227 | Initial Developer or such Contributor as a result of warranty,
228 | support, indemnity or liability terms You offer.
229 |
230 | 3.6. Distribution of Executable Versions.
231 | You may distribute Covered Code in Executable form only if the
232 | requirements of Section 3.1-3.5 have been met for that Covered Code,
233 | and if You include a notice stating that the Source Code version of
234 | the Covered Code is available under the terms of this License,
235 | including a description of how and where You have fulfilled the
236 | obligations of Section 3.2. The notice must be conspicuously included
237 | in any notice in an Executable version, related documentation or
238 | collateral in which You describe recipients' rights relating to the
239 | Covered Code. You may distribute the Executable version of Covered
240 | Code or ownership rights under a license of Your choice, which may
241 | contain terms different from this License, provided that You are in
242 | compliance with the terms of this License and that the license for the
243 | Executable version does not attempt to limit or alter the recipient's
244 | rights in the Source Code version from the rights set forth in this
245 | License. If You distribute the Executable version under a different
246 | license You must make it absolutely clear that any terms which differ
247 | from this License are offered by You alone, not by the Initial
248 | Developer or any Contributor. You hereby agree to indemnify the
249 | Initial Developer and every Contributor for any liability incurred by
250 | the Initial Developer or such Contributor as a result of any such
251 | terms You offer.
252 |
253 | 3.7. Larger Works.
254 | You may create a Larger Work by combining Covered Code with other code
255 | not governed by the terms of this License and distribute the Larger
256 | Work as a single product. In such a case, You must make sure the
257 | requirements of this License are fulfilled for the Covered Code.
258 |
259 | 4. Inability to Comply Due to Statute or Regulation.
260 |
261 | If it is impossible for You to comply with any of the terms of this
262 | License with respect to some or all of the Covered Code due to
263 | statute, judicial order, or regulation then You must: (a) comply with
264 | the terms of this License to the maximum extent possible; and (b)
265 | describe the limitations and the code they affect. Such description
266 | must be included in the LEGAL file described in Section 3.4 and must
267 | be included with all distributions of the Source Code. Except to the
268 | extent prohibited by statute or regulation, such description must be
269 | sufficiently detailed for a recipient of ordinary skill to be able to
270 | understand it.
271 |
272 | 5. Application of this License.
273 |
274 | This License applies to code to which the Initial Developer has
275 | attached the notice in Exhibit A and to related Covered Code.
276 |
277 | 6. Versions of the License.
278 |
279 | 6.1. New Versions.
280 | Netscape Communications Corporation ("Netscape") may publish revised
281 | and/or new versions of the License from time to time. Each version
282 | will be given a distinguishing version number.
283 |
284 | 6.2. Effect of New Versions.
285 | Once Covered Code has been published under a particular version of the
286 | License, You may always continue to use it under the terms of that
287 | version. You may also choose to use such Covered Code under the terms
288 | of any subsequent version of the License published by Netscape. No one
289 | other than Netscape has the right to modify the terms applicable to
290 | Covered Code created under this License.
291 |
292 | 6.3. Derivative Works.
293 | If You create or use a modified version of this License (which you may
294 | only do in order to apply it to code which is not already Covered Code
295 | governed by this License), You must (a) rename Your license so that
296 | the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
297 | "MPL", "NPL" or any confusingly similar phrase do not appear in your
298 | license (except to note that your license differs from this License)
299 | and (b) otherwise make it clear that Your version of the license
300 | contains terms which differ from the Mozilla Public License and
301 | Netscape Public License. (Filling in the name of the Initial
302 | Developer, Original Code or Contributor in the notice described in
303 | Exhibit A shall not of themselves be deemed to be modifications of
304 | this License.)
305 |
306 | 7. DISCLAIMER OF WARRANTY.
307 |
308 | COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
309 | WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
310 | WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
311 | DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
312 | THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
313 | IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
314 | YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
315 | COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
316 | OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
317 | ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
318 |
319 | 8. TERMINATION.
320 |
321 | 8.1. This License and the rights granted hereunder will terminate
322 | automatically if You fail to comply with terms herein and fail to cure
323 | such breach within 30 days of becoming aware of the breach. All
324 | sublicenses to the Covered Code which are properly granted shall
325 | survive any termination of this License. Provisions which, by their
326 | nature, must remain in effect beyond the termination of this License
327 | shall survive.
328 |
329 | 8.2. If You initiate litigation by asserting a patent infringement
330 | claim (excluding declatory judgment actions) against Initial Developer
331 | or a Contributor (the Initial Developer or Contributor against whom
332 | You file such action is referred to as "Participant") alleging that:
333 |
334 | (a) such Participant's Contributor Version directly or indirectly
335 | infringes any patent, then any and all rights granted by such
336 | Participant to You under Sections 2.1 and/or 2.2 of this License
337 | shall, upon 60 days notice from Participant terminate prospectively,
338 | unless if within 60 days after receipt of notice You either: (i)
339 | agree in writing to pay Participant a mutually agreeable reasonable
340 | royalty for Your past and future use of Modifications made by such
341 | Participant, or (ii) withdraw Your litigation claim with respect to
342 | the Contributor Version against such Participant. If within 60 days
343 | of notice, a reasonable royalty and payment arrangement are not
344 | mutually agreed upon in writing by the parties or the litigation claim
345 | is not withdrawn, the rights granted by Participant to You under
346 | Sections 2.1 and/or 2.2 automatically terminate at the expiration of
347 | the 60 day notice period specified above.
348 |
349 | (b) any software, hardware, or device, other than such Participant's
350 | Contributor Version, directly or indirectly infringes any patent, then
351 | any rights granted to You by such Participant under Sections 2.1(b)
352 | and 2.2(b) are revoked effective as of the date You first made, used,
353 | sold, distributed, or had made, Modifications made by that
354 | Participant.
355 |
356 | 8.3. If You assert a patent infringement claim against Participant
357 | alleging that such Participant's Contributor Version directly or
358 | indirectly infringes any patent where such claim is resolved (such as
359 | by license or settlement) prior to the initiation of patent
360 | infringement litigation, then the reasonable value of the licenses
361 | granted by such Participant under Sections 2.1 or 2.2 shall be taken
362 | into account in determining the amount or value of any payment or
363 | license.
364 |
365 | 8.4. In the event of termination under Sections 8.1 or 8.2 above,
366 | all end user license agreements (excluding distributors and resellers)
367 | which have been validly granted by You or any distributor hereunder
368 | prior to termination shall survive termination.
369 |
370 | 9. LIMITATION OF LIABILITY.
371 |
372 | UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
373 | (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
374 | DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
375 | OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
376 | ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
377 | CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
378 | WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
379 | COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
380 | INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
381 | LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
382 | RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
383 | PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
384 | EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
385 | THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
386 |
387 | 10. U.S. GOVERNMENT END USERS.
388 |
389 | The Covered Code is a "commercial item," as that term is defined in
390 | 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
391 | software" and "commercial computer software documentation," as such
392 | terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
393 | C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
394 | all U.S. Government End Users acquire Covered Code with only those
395 | rights set forth herein.
396 |
397 | 11. MISCELLANEOUS.
398 |
399 | This License represents the complete agreement concerning subject
400 | matter hereof. If any provision of this License is held to be
401 | unenforceable, such provision shall be reformed only to the extent
402 | necessary to make it enforceable. This License shall be governed by
403 | California law provisions (except to the extent applicable law, if
404 | any, provides otherwise), excluding its conflict-of-law provisions.
405 | With respect to disputes in which at least one party is a citizen of,
406 | or an entity chartered or registered to do business in the United
407 | States of America, any litigation relating to this License shall be
408 | subject to the jurisdiction of the Federal Courts of the Northern
409 | District of California, with venue lying in Santa Clara County,
410 | California, with the losing party responsible for costs, including
411 | without limitation, court costs and reasonable attorneys' fees and
412 | expenses. The application of the United Nations Convention on
413 | Contracts for the International Sale of Goods is expressly excluded.
414 | Any law or regulation which provides that the language of a contract
415 | shall be construed against the drafter shall not apply to this
416 | License.
417 |
418 | 12. RESPONSIBILITY FOR CLAIMS.
419 |
420 | As between Initial Developer and the Contributors, each party is
421 | responsible for claims and damages arising, directly or indirectly,
422 | out of its utilization of rights under this License and You agree to
423 | work with Initial Developer and Contributors to distribute such
424 | responsibility on an equitable basis. Nothing herein is intended or
425 | shall be deemed to constitute any admission of liability.
426 |
427 | 13. MULTIPLE-LICENSED CODE.
428 |
429 | Initial Developer may designate portions of the Covered Code as
430 | "Multiple-Licensed". "Multiple-Licensed" means that the Initial
431 | Developer permits you to utilize portions of the Covered Code under
432 | Your choice of the NPL or the alternative licenses, if any, specified
433 | by the Initial Developer in the file described in Exhibit A.
434 |
435 | EXHIBIT A -Mozilla Public License.
436 |
437 | ``The contents of this file are subject to the Mozilla Public License
438 | Version 1.1 (the "License"); you may not use this file except in
439 | compliance with the License. You may obtain a copy of the License at
440 | http://www.mozilla.org/MPL/
441 |
442 | Software distributed under the License is distributed on an "AS IS"
443 | basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
444 | License for the specific language governing rights and limitations
445 | under the License.
446 |
447 | The Original Code is ______________________________________.
448 |
449 | The Initial Developer of the Original Code is ________________________.
450 | Portions created by ______________________ are Copyright (C) ______
451 | _______________________. All Rights Reserved.
452 |
453 | Contributor(s): ______________________________________.
454 |
455 | Alternatively, the contents of this file may be used under the terms
456 | of the _____ license (the "[___] License"), in which case the
457 | provisions of [______] License are applicable instead of those
458 | above. If you wish to allow use of your version of this file only
459 | under the terms of the [____] License and not to allow others to use
460 | your version of this file under the MPL, indicate your decision by
461 | deleting the provisions above and replace them with the notice and
462 | other provisions required by the [___] License. If you do not delete
463 | the provisions above, a recipient may use your version of this file
464 | under either the MPL or the [___] License."
465 |
466 | [NOTE: The text of this Exhibit A may differ slightly from the text of
467 | the notices in the Source Code files of the Original Code. You should
468 | use the text of this Exhibit A rather than the text found in the
469 | Original Code Source Code for Your Modifications.]
470 |
471 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pencil
2 |
3 | *NOTE* If you are checking out pencil for the first time you should check out the
4 | rewrite branch (in beta) which has lots of nice updates.
5 |
6 | pencil is a monitoring frontend for graphite. It runs a webserver that dishes
7 | out pretty graphite urls in hopefully interesting and intuitive layouts.
8 |
9 | Some features are:
10 |
11 | * Easy configuration
12 |
13 | Pretty much anything you'd want to do with the graphite composer can be coded
14 | into pencil configs using YAML syntax.
15 |
16 | * Implicit collection of host and cluster data
17 |
18 | pencil picks these up from graphite without you having to explicitly define
19 | them. You need only supply the metrics; see for
20 | configuration details.
21 |
22 | * Relative and absolute timespecs with auto-refresh
23 |
24 | Timeslices are measured in terms of a (possibly relative) starting time and a
25 | duration. Timespecs are parsed using the
26 | [chronic](http://chronic.rubyforge.org/) and
27 | [chronic_duration](https://github.com/hpoydar/chronic_duration) gems.
28 |
29 | You can use pencil in "tail-mode" (i.e. constant refresh, looking at last
30 | couple hours of data) or to view a particular timeslice in the past.
31 |
32 | * time-quantum support
33 |
34 | Requests can be mapped into discrete intervals (e.g.. 30 seconds) so that all
35 | requests within a window are treated as if they are the same request. This
36 | eases the burden on graphite to generate real-time images that are only
37 | slightly different than those generated by earlier requests. It also makes it
38 | easy to add some sort of caching layer.
39 |
40 | * permalinks
41 |
42 | Turn a relative timeslice (such as the last 8 hours) into an absolute one for
43 | placing in bug reports and all sorts of other useful things.
44 |
45 | * Lots of views and navigation UI for bouncing around them
46 |
47 | Global, cluster, dashboard, and host views!
48 |
49 | ## INSTALL
50 |
51 | gem install pencil
52 |
53 | Dependencies are:
54 |
55 | * rack
56 | * sinatra
57 | * mongrel
58 | * json
59 | * chronic
60 | * chronic_duration
61 | * (fixme versions)
62 |
63 | ## SETUP
64 |
65 | You should have a working graphite installation. Your metrics need to be
66 | composed of three pieces:
67 |
68 | * "%m", _METRIC_ (the common part of each graphite path)
69 | * "%c", _CLUSTER_ (cluster name, varies with query, must not contain periods)
70 | * "%h", _HOST_ (host name, varies with query, must not contain periods)
71 |
72 | The :metric_format string is specified in a configuration file (see below), and
73 | defaults to %m.%c.%h". It should contain only one %m, but is otherwise mostly
74 | unrestricted.
75 |
76 | You need to set up YAML configuration files in order for pencil to work. Pencil
77 | searches the current directory (or, with -d DIR, DIR) for YAML files to load.
78 |
79 | The important top-level configuration keys are:
80 |
81 | * :config:
82 | * :graphs:
83 | * :dashboards:
84 |
85 | See examples/ for an example configuration directory. Here's
86 | an example pencil.yml, which contains general configuration options:
87 |
88 | :config:
89 | :graphite_url URL # graphite URL
90 | :url_opts
91 | :width: 1000
92 | :height: 400
93 | :fontSize: 15
94 | :start: "8 hours ago" # in chronic timespec format
95 | :template: "noc"
96 | :yMin: 0
97 | :margin: 5
98 | :thickness: 2
99 |
100 | :refresh_rate: 60 # how often to refresh the view
101 | :host_sort: "numeric" # add this if you want to sort hosts numerically
102 | :quantum: 30 # map requests to 30 second intervals
103 | :date_format: "%X %x" # strftime
104 | :metric_format: "%m.%c.%h" #%m metric, %c cluster, %h host
105 |
106 | A graph is a name, title, collection of targets, and some other options.
107 | A target is a metric => options map; see docs/pencil_options.md for details on
108 | supported options. It looks like (in YAML):
109 |
110 | graph_name: # name pencil references this graph by
111 | title: "graph_title" # title displayed on graph
112 | targets: # list of graphite targets
113 | metric.1:
114 | :key: key # key displayed on the legend for this metric
115 | :color: color # color on the graph for this metric
116 | metric.2:
117 | :key: key
118 | :color: color
119 | [...]
120 | hosts: ["hosts1*", "test*_web", "hosts2*"] # filter on hosts
121 | areaMode: stacked
122 |
123 | (Note that in any case where you would use a hash you may use an
124 | [omap](http://www.yaml.org/spec/current.html#id2504191) instead, to ensure
125 | the order in which options are applied)
126 |
127 | Graph-level options are applied to the graph as a whole, where target-level
128 | options apply only to the specific target.
129 |
130 | Similarly, a dashboard is a name, title, collection of graphs, and some other
131 | options. It looks like (in YAML):
132 |
133 | dash_name:
134 | title: dash_title
135 | graphs:
136 | - graph1:
137 | hosts: ["sync*_web"] # hosts filter specific to this graph
138 | other_opt: val # possibly other options in the future
139 | - graph2:
140 | [...]
141 | hosts: ["filter1*", "*filter2"]
142 |
143 | A graph is just a graph name => options hash. Options specified in a dashboard
144 | for a graph override the options in the original graph's definition when
145 | displaying the dashboard.
146 |
147 | Pencil loads all the yaml files in its configuration directory and
148 | subdirectories. To facilitate organization of graphs and dashboards into
149 | multiple files the :graphs and :dashboards top-level keys are merged
150 | recursively during the load. The resulting pencil data structure will
151 | include all graphs and dashboards defined under these keys.
152 |
153 | A few words on host filters: two wildcards are supported: "*" and "#".
154 | "*" consumes as /.*/ (like a shell wildcard) and "#" as /\d+/ (one or more
155 | digits). Be aware that "#" may require explicit enumeration in graphite URLs
156 | (and is currently implemented in this way) and you might have to configure
157 | Apache to accept longer URLs if you have many hosts that match a "#".
158 |
159 | ### complex metrics and dashboard graph-level options
160 | A simple target is just a metric => options map. Targets can also be complex,
161 | where the key is an ordered list of simple targets. This is useful, for
162 | example, if you want to graph the summation of a list of metrics (see
163 | docs/pencil_options.md for a list of supported transforms). Complex targets are
164 | denoted with the YAML's
165 | [? : complex-key syntax](http://www.yaml.org/spec/current.html#id2503185), and
166 | look like this:
167 |
168 | memory_graph:
169 | title: "memory usage"
170 | targets:
171 | ? - system.memory.total:
172 | opt1: value
173 | opt2:
174 | - system.memory.free:
175 | - system.memory.cached:
176 | - system.memory.shared:
177 | - system.memory.buffers:
178 | :
179 | :key: "memory used"
180 | :color: green
181 | :diffSeries:
182 |
183 | As you can see, the memory used is computed by taking the difference of the
184 | total memory and its many uses.
185 |
186 | ## RUNNING THE SERVER
187 | Once you've set up the configs, you should be able to just run
188 |
189 | pencil
190 |
191 | and get something up on localhost:9292
192 |
193 | From there you should be able to click around and see various interesting
194 | collections of graphs.
195 |
196 | With no options, pencil looks in the current directory for YAML files and loads
197 | them.
198 |
199 | You can bind to a specific port with -p PORT and specify a configuration
200 | directory with -d DIR. Other rack-related options will be added at some point
201 | in the future.
202 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | require "bundler/gem_tasks"
3 |
--------------------------------------------------------------------------------
/bin/pencil:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pencil'
3 |
4 | Pencil::App.run!
5 |
--------------------------------------------------------------------------------
/docs/pencil_options.md:
--------------------------------------------------------------------------------
1 | # Pencil Options
2 | Pencil configuration files are written in YAML. When pencil starts up, it
3 | searches for these files and loads them. See the main README.md for how
4 | these files should look.
5 |
6 | ## General Configuration
7 |
8 | These are options that go under the :config key in pencil configuration files.
9 |
10 | * :graphite_url [String, required, no default]
11 |
12 | The url of your graphite instance.
13 |
14 | * :url_opts [Hash, required, no default]
15 |
16 | A map of default graph options.
17 |
18 | In addition to graph-level options, an important default option
19 | you should set under :url_opts is
20 |
21 | :start: TIMESPEC
22 |
23 | TIMESPEC should be a
24 | [chronic](http://chronic.rubyforge.org/)-parsable time, and to be useful
25 | should be relative to the current time (e.g. "8 hours ago")
26 |
27 | * :refresh_rate [Fixnum, optional, default 60]
28 |
29 | How often to refresh a changing view, in seconds.
30 |
31 | This doesn't apply to timeslices that aren't varying (i.e not current, see
32 | :now_threshold).
33 |
34 | Set this to false to disable automatic refreshing.
35 |
36 | * :host_sort [["builtin", "numeric", "sensible"], optional, default "sensible"]
37 |
38 | Set to "builtin" to sort using ruby's builtin String sort.
39 |
40 | Set to "numeric" to sort hosts numerically (i.e. match secondarily on the
41 | first \d+).
42 |
43 | Set to "sensible" if you want to sort like this:
44 |
45 | http://www.bofh.org.uk/2007/12/16/comprehensible-sorting-in-ruby
46 |
47 | * :quantum [Fixnum, optional, no default value]
48 |
49 | Map requests to NUM second intervals. Pencil floors request times to the
50 | minute, and does some modular arithmetic to do this mapping. This is
51 | especially useful for implementing a caching layer, so that many requests
52 | coming in near-simultaneously won't require graphite to generate different
53 | images for each request.
54 |
55 | Adding &noq=1 to a pencil url will disable this in case you need
56 | super-granularity for some reason, but you didn't hear it from me.
57 |
58 | * :date_format [String, optional, default "%X %x"]
59 |
60 | strftime format for displaying dates.
61 |
62 | * :metric_format [String, optional, default "%m.%c.%h"]
63 |
64 | The format your graphite metrics are stored in. For pencil to work your
65 | metrics need to be composed of three distinct pieces, concatenated in some
66 | regular fashion. The format strings are
67 |
68 | * %m metric
69 | * %c cluster
70 | * %h host
71 |
72 | If you want a literal %[mch] in your metric format string you likely have
73 | bigger problems than not being able to do so.
74 |
75 | * :now\_threshold: [Fixnum, optional, default 300]
76 |
77 | How many seconds before Time.now an end time is considered to still be 'now',
78 | for the purposes of adding meta-refresh and displaying time intervals.
79 |
80 | * :use_color: [Boolean, optional, default false]
81 |
82 | In graphite 0.9.8 and earlier coloring of targets sucks. Coloring is not done
83 | on a per-target basis, but rather by an additional parameter
84 | "colorList". Pencil handles colors by appending a target's color parameter
85 | (or a suitable default) onto this list. Targets are processed in order and
86 | consume the first color available in colorList.
87 |
88 | This sucks because nonexistent targets occasionally produced by pencil don't
89 | consume their associated color, which screws up the coloring of every
90 | subsequent metric.
91 |
92 | The latest Graphite trunk supports color as a property of a target, set with
93 | color(target, "COLOR"). If you are using a new graphite and want to take
94 | advantage of this more accurate coloring technique set :use_color to true,
95 | and bask in the glory of color correctness.
96 |
97 | ## Graph-level Options
98 | This is a list of the supported graph-level options for pencil, which
99 | correspond to request(image)-level options for graphite. These options are
100 | key-value pairs, and are passed directly to graphite. Here is the list, with
101 | minor annotations:
102 |
103 | * vtitle: String (y-axis label)
104 | * yMin: Fixnum
105 | * yMax: Fixnum
106 | * lineWidth: Fixnum (line thickness in pixels)
107 | * areaMode: \[first, all, stacked\] (see graphite documentation)
108 | * template: \[noc, alphas\] (alphas inverts colors)
109 | * lineMode: staircase
110 | * bgcolor: String
111 | * graphOnly: bool (hide legend, axes, grid)
112 | * hideAxes: bool
113 | * hideGrid: bool
114 | * hideLegend: bool
115 | * fgcolor: String
116 | * fontSize: Fixnum
117 | * fontName: String (see your graphite instance for available fonts)
118 | * fontItalic: bool
119 | * fontBold: bool
120 |
121 | ## Target-level Options
122 | This is a list of the supported target-level options for pencil. These are
123 | mostly a list of transformations graphite supports, including summation and
124 | scaling of metrics. You can apply them to individual metrics, or lists of
125 | metrics. See the example configs for how this works. Also see the graphite
126 | composer for the effects of these options, many of which are untested.
127 |
128 | A note on the divideSeries case: for aggregate graphs, pencil takes the ratio
129 | of the sums, as opposed to the sums of the ratios, for ease of implementation
130 | and because it makes some sense. If you're not a fan of 80k URLs then you will
131 | agree. divideSeries should only ever be used with two targets, like this:
132 |
133 | ? - stats.timers.mysql.innodb.threads_active_read.mean:
134 | - stats.timers.mysql.innodb.threads_total_read.mean:
135 | :
136 | !omap
137 | - :divideSeries:
138 |
139 | (Notice the omap, as opposed to a hash, to impose an order in which options are
140 | applied)
141 |
142 | ### Combinations
143 | These functions take an arbitrary number of targets (usually simple metrics)
144 | for arguments.
145 |
146 | * sumSeries
147 | * averageSeries
148 | * minSeries
149 | * maxSeries
150 | * group
151 |
152 | ### Transformations
153 | Some of these options take a single argument.
154 |
155 | * scale
156 | * offset
157 | * derivative
158 | * integral
159 | * nonNegativeDerivative
160 | * log BASE
161 | * timeShift
162 | * summarize
163 | * hitcount
164 |
165 | ### Calculations
166 | These functions take an arbitrary number of targets (usually simple metrics)
167 | for arguments.
168 |
169 | * movingAverage
170 | * stdev
171 | * asPercent
172 | * diffSeries
173 | * divideSeries (NOTE: takes exactly 1 series as divisor)
174 |
175 | ### Filters
176 | Most of these options take a single argument.
177 |
178 | * highestCurrent
179 | * lowestCurrent
180 | * nPercentile
181 | * currentAbove
182 | * currentBelow
183 | * highestAverage
184 | * lowestAverage
185 | * averageAbove
186 | * averageBelow
187 | * maximumAbove
188 | * maximumBelow
189 | * sortByMaxima
190 | * minimalist
191 | * limit
192 | * exclude
193 |
194 | ### Special Operations
195 | * alias
196 | * key (alias for alias)
197 | * cumulative
198 | * drawAsInfinite
199 | * lineWidth
200 | * dashed
201 | * keepLastValue
202 | * substr
203 | * threshold
204 | * color
205 |
206 | Note: key and color are applied after all other options are applied.
207 |
208 | key is interpreted differently from the other options, which are more
209 | simply translated.
210 |
211 | color's interpretation depends on whether :use_color is set.
212 |
--------------------------------------------------------------------------------
/examples/dashboards.yml:
--------------------------------------------------------------------------------
1 | ---
2 | :dashboards:
3 | syncstorage:
4 | title: syncstorage_overview
5 | graphs:
6 | - syncstorage_qps:
7 | - syncstorage_401:
8 | - syncstorage_exceptions:
9 | - cpu_usage:
10 | - memory_usage:
11 | - network_traffic:
12 | hosts: ["sync*_web", "wp-web*"]
13 | syncreg:
14 | title: syncreg_overview
15 | graphs:
16 | - syncreg_qps:
17 | - syncreg_exceptions:
18 | - cpu_usage:
19 | - memory_usage:
20 | - network_traffic:
21 | hosts: ["wp-reg*"]
22 | syncsreg:
23 | title: syncsreg_overview
24 | graphs:
25 | - syncsreg_qps:
26 | - syncsreg_exceptions:
27 | - cpu_usage:
28 | - memory_usage:
29 | - network_traffic:
30 | hosts: ["wp-sreg*"]
31 | ldap_slaves:
32 | title: LDAP slaves
33 | graphs:
34 | - ldap_ops:
35 | - load_average:
36 | - cpu_usage:
37 | - network_traffic:
38 | hosts: ["slave*_ldap", "wp-slave*"]
39 | ldap_masters:
40 | title: LDAP masters
41 | graphs:
42 | - ldap_ops:
43 | - load_average:
44 | - cpu_usage:
45 | - network_traffic:
46 | hosts: ["master*_ldap", "wp-master*"]
47 | sync_mysql:
48 | title: sync mysql
49 | graphs:
50 | - mysql_ops:
51 | - load_average:
52 | - cpu_usage:
53 | - network_traffic:
54 | - mysql_pending_file_io:
55 | - mysql_history_length:
56 | - mysql_file_io_threads:
57 | - mysql_file_io_threads_usage:
58 | hosts: ["sync*_db", "wp-db*"]
59 | sync_node_alloc:
60 | title: sync node allocation
61 | graphs:
62 | - sync_node_alloc:
63 | - sync_node_actives:
64 | hosts: ["wp-adm01"]
65 |
--------------------------------------------------------------------------------
/examples/graphs.yml:
--------------------------------------------------------------------------------
1 | ---
2 | :graphs:
3 | syncstorage_qps:
4 | title: "syncstorage qps"
5 | targets:
6 | syncstorage.request_rate.200:
7 | :key: 200
8 | :color: green
9 | syncstorage.request_rate.302:
10 | :key: 302
11 | :color: blue
12 | syncstorage.request_rate.401:
13 | :key: 401
14 | :color: brown
15 | syncstorage.request_rate.404:
16 | :key: 404
17 | :color: purple
18 | syncstorage.request_rate.503:
19 | :key: 503
20 | :color: red
21 | areaMode: stacked
22 | hosts: ["sync*_web", "test*_web", "wp-web*"]
23 |
24 | syncstorage_401:
25 | title: "syncstorage 401s"
26 | targets:
27 | syncstorage.request_rate.401:
28 | :key: 200
29 | :color: brown
30 | hosts: ["sync*_web", "test*_web", "wp-web*"]
31 |
32 | syncstorage_exceptions:
33 | title: "syncstorage exceptions"
34 | targets:
35 | syncstorage.log.exception:
36 | :key: exceptions/s
37 | :color: red
38 | hosts: ["sync*_web", "test*_web", "wp-web*"]
39 |
40 | syncreg_qps:
41 | title: "syncreg qps"
42 | targets:
43 | syncreg.request_rate.200:
44 | :key: 200
45 | :color: green
46 | syncreg.request_rate.302:
47 | :key: 302
48 | :color: blue
49 | syncreg.request_rate.401:
50 | :key: 401
51 | :color: brown
52 | syncreg.request_rate.404:
53 | :key: 404
54 | :color: purple
55 | syncreg.request_rate.503:
56 | :key: 503
57 | :color: red
58 | areaMode: stacked
59 | hosts: ["wp-reg*"]
60 |
61 | syncreg_exceptions:
62 | title: "syncreg exceptions"
63 | targets:
64 | syncreg.log.exception:
65 | :key: exceptions/s
66 | :color: red
67 | hosts: ["wp-reg*"]
68 |
69 | syncsreg_qps:
70 | title: "syncsreg qps"
71 | targets:
72 | syncsreg.request_rate.200:
73 | :key: 200
74 | :color: green
75 | syncsreg.request_rate.302:
76 | :key: 302
77 | :color: blue
78 | syncsreg.request_rate.401:
79 | :key: 401
80 | :color: brown
81 | syncsreg.request_rate.404:
82 | :key: 404
83 | :color: purple
84 | syncsreg.request_rate.503:
85 | :key: 503
86 | :color: red
87 | areaMode: stacked
88 | hosts: ["wp-sreg*"]
89 |
90 | syncsreg_exceptions:
91 | title: "syncsreg exceptions"
92 | targets:
93 | syncsreg.log.exception:
94 | :key: exceptions/s
95 | :color: red
96 | hosts: ["wp-sreg*"]
97 |
98 | cpu_usage:
99 | title: "cpu usage"
100 | targets:
101 | system.cpu.system:
102 | :key: CPU/system
103 | :color: yellow
104 | system.cpu.wio:
105 | :key: CPU/wio
106 | :color: red
107 | system.cpu.nice:
108 | :key: CPU/nice
109 | :color: green
110 | system.cpu.user:
111 | :key: CPU/user
112 | :color: blue
113 | areaMode: stacked
114 | hosts: ["*"]
115 |
116 | load_average:
117 | title: "1 minute load average"
118 | targets:
119 | system.load.1:
120 | :key: load
121 | hosts: ["*"]
122 |
123 | network_traffic:
124 | title: "network traffic"
125 | targets:
126 | system.network.in:
127 | :key: bits in
128 | :color: green
129 | system.network.out:
130 | :key: bits out
131 | :color: blue
132 |
133 | hosts: ["*"]
134 | scale: 8 # bytes -> bits
135 |
136 | ldap_ops:
137 | title: "ldap operations"
138 | targets:
139 | slapd.ops.add:
140 | :key: add
141 | :color: red
142 | slapd.ops.bind:
143 | :key: bind
144 | :color: brown
145 | slapd.ops.modify:
146 | :key: modify
147 | :color: purple
148 | slapd.ops.search:
149 | :key: search
150 | :color: green
151 |
152 | areaMode: stacked
153 | hosts: ["slave*ldap", "master*ldap", "wp-master*"]
154 |
155 | mysql_ops:
156 | title: "mysql operations"
157 | targets:
158 | mysql.ops.delete:
159 | :key: delete
160 | :color: red
161 | mysql.ops.insert:
162 | :key: insert
163 | :color: brown
164 | mysql.ops.update:
165 | :key: update
166 | :color: purple
167 | mysql.ops.select:
168 | :key: select
169 | :color: green
170 |
171 | areaMode: stacked
172 | hosts: ["sync*_db", "wp-db*"]
173 |
174 | memory_usage:
175 | title: "system memory usage"
176 | targets:
177 | ? - system.memory.total:
178 | - system.memory.free:
179 | - system.memory.cached:
180 | - system.memory.shared:
181 | - system.memory.buffers:
182 | :
183 | :key: used
184 | :color: green
185 | :diffSeries:
186 | system.memory.total:
187 | :key: total
188 | :color: white
189 | system.memory.cached:
190 | :key: cached
191 | :color: purple
192 |
193 | scale: 1024 # M -> G
194 | hosts: ["*"]
195 | # some random options for demonstration
196 | # template: alphas
197 | # bgcolor: "#AFFFFF"
198 | # vtitle: tvtnolgrf
199 | # lineWidth: 20
200 |
201 | sync_node_alloc:
202 | title: "node alloc rate"
203 | targets:
204 | sync.users.alloc_rate.scl2:
205 | :key: scl2 alloc/s
206 | :color: green
207 | sync.users.alloc_rate.phx1:
208 | :key: phx1 alloc/s
209 | :color: blue
210 |
211 | hosts: ["wp-adm01"]
212 |
213 | sync_node_actives:
214 | title: "node actives"
215 | targets:
216 | sync.users.active.scl2:
217 | :key: scl2 actives
218 | :color: green
219 | sync.users.active.phx1:
220 | :key: phx1 actives
221 | :color: blue
222 |
223 | hosts: ["wp-adm01"]
224 |
225 | mysql_pending_file_io:
226 | title: "mysql pending file i/o"
227 | targets:
228 | stats.timers.mysql.innodb.pending_read.mean:
229 | :key: read
230 | :color: green
231 | stats.timers.mysql.innodb.pending_write.mean:
232 | :key: write
233 | :color: blue
234 | stats.timers.mysql.innodb.pending_ibuf.mean:
235 | :key: ibuf
236 | :color: brown
237 | stats.timers.mysql.innodb.pending_log.mean:
238 | :key: log
239 | :color: red
240 | stats.timers.mysql.innodb.pending_sync.mean:
241 | :key: sync
242 | :color: yellow
243 |
244 | hosts: ["wp-db*", "sync*_db"]
245 |
246 | mysql_history_length:
247 | title: "mysql transaction history"
248 | targets:
249 | stats.timers.mysql.innodb.history_length.mean:
250 | :key: read
251 | :color: green
252 |
253 | hosts: ["wp-db*", "sync*_db"]
254 |
255 | mysql_file_io_threads_usage:
256 | title: "mysql file i/o threads active (% used)"
257 | targets:
258 | ? - stats.timers.mysql.innodb.threads_active_read.mean:
259 | - stats.timers.mysql.innodb.threads_total_read.mean:
260 | :
261 | !omap
262 | - :divideSeries:
263 | - :color: green
264 | - :asPercent: 1.0
265 | - :movingAverage: 10
266 | - :key: read
267 | ? - stats.timers.mysql.innodb.threads_active_write.mean:
268 | - stats.timers.mysql.innodb.threads_total_write.mean:
269 | :
270 | !omap
271 | - :divideSeries:
272 | - :color: blue
273 | - :asPercent: 1.0
274 | - :movingAverage: 10
275 | - :key: write
276 | ? - stats.timers.mysql.innodb.threads_active_ibuf.mean:
277 | - stats.timers.mysql.innodb.threads_total_ibuf.mean:
278 | :
279 | !omap
280 | - :divideSeries:
281 | - :color: brown
282 | - :asPercent: 1.0
283 | - :movingAverage: 10
284 | - :key: ibuf
285 | ? - stats.timers.mysql.innodb.threads_active_log.mean:
286 | - stats.timers.mysql.innodb.threads_total_log.mean:
287 | :
288 | !omap
289 | - :divideSeries:
290 | - :color: red
291 | - :asPercent: 1.0
292 | - :movingAverage: 10
293 | - :key: log
294 | hosts: ["wp-db*", "sync*_db"]
295 |
296 | mysql_file_io_threads:
297 | title: "mysql file i/o threads"
298 | targets:
299 | stats.timers.mysql.innodb.threads_active_read.mean:
300 | !omap
301 | - :color: green
302 | - :movingAverage: 10
303 | - :key: read
304 | stats.timers.mysql.innodb.threads_active_write.mean:
305 | !omap
306 | - :color: blue
307 | - :movingAverage: 10
308 | - :key: write
309 | stats.timers.mysql.innodb.threads_active_ibuf.mean:
310 | !omap
311 | - :color: brown
312 | - :movingAverage: 10
313 | - :key: ibuf
314 | stats.timers.mysql.innodb.threads_active_log.mean:
315 | !omap
316 | - :color: red
317 | - :movingAverage: 10
318 | - :key: log
319 |
320 | hosts: ["wp-db*", "sync*_db"]
321 |
--------------------------------------------------------------------------------
/examples/pencil.yml:
--------------------------------------------------------------------------------
1 | ---
2 | :config:
3 | :graphite_url: http://graphite-global.phx.weave.mozilla.com
4 | :default_colors:
5 | ["blue", "green", "yellow", "red", "purple", "brown", "aqua", "gold"]
6 | :url_opts:
7 | :width: 1000
8 | :height: 400
9 | :fontSize: 15
10 | :start: "8 hours ago" # chronic timespec
11 | :template: "noc"
12 | :yMin: 0
13 | :margin: 5
14 | :thickness: 2
15 |
16 | :refresh_rate: 60
17 | :host_sort: "sensible"
18 | :quantum: 30 # map requests to 30 second intervals
19 | :date_format: "%X %x" # strftime
20 | :metric_format: "%m.%c.%h" #%m metric, %c cluster, %h host
21 | :now_threshold: 300 # Time.at(Time.now - now_threshold) considered Time.now
22 |
23 | # set this if you have a new graphite that supports color(series, "color")
24 | # :use_color: true
25 |
--------------------------------------------------------------------------------
/lib/pencil.rb:
--------------------------------------------------------------------------------
1 | require "erb"
2 | require "rack"
3 | require "sinatra/base"
4 | require "json"
5 | require "open-uri"
6 | require "yaml"
7 | require "chronic"
8 | require "chronic_duration"
9 | require "optparse"
10 |
11 | require "pencil/version"
12 | require "pencil/config"
13 | require "pencil/helpers"
14 | require "pencil/models"
15 | require "pencil/rubyfixes"
16 |
17 | module Pencil
18 | class App < Sinatra::Base
19 | include Pencil::Models
20 | helpers Pencil::Helpers
21 | config = Pencil::Config.new
22 | set :config, config
23 | set :port, config.global_config[:port]
24 | set :run, true
25 | use Rack::Session::Cookie, :expire_after => 126227700 # 4 years
26 | set :root, File.expand_path('../pencil', __FILE__)
27 | set :static, true
28 | set :logging, true
29 | set :erb, :trim => '-'
30 |
31 | def initialize(settings={})
32 | super
33 | end
34 |
35 | before do
36 | session[:not] #fixme kludge is back
37 | @request_time = Time.now
38 | @dashboards = Dashboard.all
39 | @no_graphs = false
40 | # time stuff
41 | start = param_lookup("start")
42 | duration = param_lookup("duration")
43 |
44 | @stime = Chronic.parse(start)
45 | if @stime
46 | @stime -= @stime.sec unless @params["noq"]
47 | elsif start =~ /^\d+$/ #epoch
48 | @stime = Time.at start.to_i
49 | end
50 |
51 | if duration
52 | @duration = ChronicDuration.parse(duration) || 0
53 | else
54 | @duration = @request_time.to_i - @stime.to_i
55 | end
56 |
57 | unless @params["noq"]
58 | @duration -= (@duration % settings.config.global_config[:quantum]||1)
59 | end
60 |
61 | if @stime
62 | @etime = Time.at(@stime + @duration)
63 | @etime = @request_time if @etime > @request_time
64 | else
65 | @etime = @request_time
66 | end
67 |
68 | session[:stime] = @stime.to_i.to_s
69 | session[:etime] = @etime.to_i.to_s
70 | # fixme reload hosts after some expiry
71 | end
72 |
73 | get %r[^/(dash/?)?$] do
74 | @no_graphs = true
75 | if settings.config.clusters.size == 1
76 | redirect append_query_string("/dash/#{settings.config.clusters.first}")
77 | else
78 | redirect append_query_string('/dash/global')
79 | end
80 | end
81 |
82 | get '/dash/:cluster/:dashboard/:zoom/?' do
83 | @cluster = params[:cluster]
84 | @dash = Dashboard.find(params[:dashboard])
85 | raise "Unknown dashboard: #{params[:dashboard].inspect}" unless @dash
86 |
87 | @zoom = nil
88 | @dash.graphs.each do |graph|
89 | @zoom = graph if graph.name == params[:zoom]
90 | end
91 | raise "Unknown zoom parameter: #{params[:zoom]}" unless @zoom
92 |
93 | @title = "dashboard :: #{@cluster} :: #{@dash['title']} :: #{params[:zoom]}"
94 |
95 | if @cluster == "global"
96 | erb :'dash-global-zoom'
97 | else
98 | erb :'dash-cluster-zoom'
99 | end
100 | end
101 |
102 | get '/dash/:cluster/:dashboard/?' do
103 | @cluster = params[:cluster]
104 | @dash = Dashboard.find(params[:dashboard])
105 | raise "Unknown dashboard: #{params[:dashboard].inspect}" unless @dash
106 |
107 | @title = "dashboard :: #{@cluster} :: #{@dash['title']}"
108 |
109 | if @cluster == "global"
110 | erb :'dash-global'
111 | else
112 | erb :'dash-cluster'
113 | end
114 | end
115 |
116 | get '/dash/:cluster/?' do
117 | @no_graphs = true
118 | @cluster = params[:cluster]
119 | if @cluster == "global"
120 | @title = "Overview"
121 | erb :global
122 | else
123 | @title = "cluster :: #{params[:cluster]}"
124 | erb :cluster
125 | end
126 | end
127 |
128 | get '/host/:cluster/:host/?' do
129 | @host = Host.find_by_name_and_cluster(params[:host], params[:cluster])
130 | @cluster = @host.cluster
131 | raise "Unknown host: #{params[:host]} in #{params[:cluster]}" unless @host
132 |
133 | @title = "#{@host.cluster} :: host :: #{@host.name}"
134 |
135 | erb :host
136 | end
137 |
138 | get '/process' do
139 | case params["action"]
140 | when "Save"
141 | # fixme make sure not to save shitty values for :start
142 | puts 'saving prefs'
143 | params.each do |k ,v|
144 | next if [:etime, :stime, :duration].member?(k.to_sym)
145 | session[k] = v unless v.empty?
146 | end
147 | redirect URI.parse(request.referrer).path
148 | when "Clear"
149 | puts URI.parse(request.referrer).path
150 | redirect URI.parse(request.referrer).path
151 | when "Reset"
152 | puts "clearing prefs"
153 | session.clear
154 | redirect URI.parse(request.referrer).path
155 | when "Submit"
156 | # fixme offensive to sensibility
157 | redirect URI.parse(request.referrer).path + "?" + \
158 | request.query_string.sub("&action=Submit", "").sub("?action=Submit", "")
159 | end
160 | end
161 | end # Pencil::App
162 | end # Pencil
163 |
--------------------------------------------------------------------------------
/lib/pencil/config.rb:
--------------------------------------------------------------------------------
1 | require 'map'
2 | require "pencil/models"
3 |
4 | module Pencil
5 | class Config
6 | include Pencil::Models
7 |
8 | attr_reader :dashboards
9 | attr_reader :graphs
10 | attr_reader :hosts
11 | attr_reader :clusters
12 | attr_reader :global_config
13 |
14 | def initialize
15 | port = 9292
16 | @rawconfig = {}
17 | @confdir = "."
18 | @recursive = false
19 |
20 | optparse = OptionParser.new do |o|
21 | o.on("-d", "--config-dir DIR",
22 | "location of the config directory (default .)") do |arg|
23 | @confdir = arg
24 | @recursive = true
25 | end
26 | o.on("-p", "--port PORT", "port to bind to (default 9292)") do |arg|
27 | port = arg.to_i
28 | end
29 | end
30 |
31 | optparse.parse!
32 | reload!
33 | @global_config[:port] = port
34 | end
35 |
36 | def reload!
37 | # only do a recursive search if "-d" is specified
38 | configs = Dir.glob("#{@confdir}/#{@recursive ? '**/' : ''}*.y{a,}ml")
39 |
40 | configs.each do |c|
41 | yml = YAML.load(File.read(c))
42 | next unless yml
43 | @rawconfig[:config] = yml[:config] if yml[:config]
44 | a = @rawconfig[:dashboards]
45 | b = yml[:dashboards]
46 | c = @rawconfig[:graphs]
47 | d = yml[:graphs]
48 |
49 | if a && b
50 | a.merge!(b)
51 | elsif b
52 | @rawconfig[:dashboards] = b
53 | end
54 | if c && d
55 | c.merge!(d)
56 | elsif d
57 | @rawconfig[:graphs] = d
58 | end
59 | end
60 | @rawconfig = Map(@rawconfig)
61 |
62 | [:graphs, :dashboards, :config].each do |c|
63 | if not @rawconfig[c.to_s]
64 | raise "Missing config name '#{c.to_s}'"
65 | end
66 | end
67 |
68 | @global_config = @rawconfig[:config]
69 | # do some sanity checking of other configuration parameters
70 | [:graphite_url, :url_opts].each do |c|
71 | if not @global_config[c]
72 | raise "Missing config name '#{c.to_s}'"
73 | end
74 | end
75 |
76 | # possibly check more url_opts here as well
77 | if @global_config[:url_opts][:start]
78 | if !ChronicDuration.parse(@global_config[:url_opts][:start])
79 | raise "bad default timespec in :url_opts"
80 | end
81 | end
82 |
83 | @global_config[:default_colors] ||=
84 | ["blue", "green", "yellow", "red", "purple", "brown", "aqua", "gold"]
85 |
86 | if @global_config[:refresh_rate]
87 | duration = ChronicDuration.parse(@global_config[:refresh_rate].to_s)
88 | if !duration
89 | raise "couldn't parse key :refresh_rate"
90 | end
91 | @global_config[:refresh_rate] = duration
92 | end
93 |
94 | @global_config[:metric_format] ||= "%m.%c.%h"
95 | if @global_config[:metric_format] !~ /%m/
96 | raise "missing metric (%m) in :metric_format"
97 | elsif @global_config[:metric_format] !~ /%c/
98 | raise "missing cluster (%c) in :metric_format"
99 | elsif @global_config[:metric_format] !~ /%h/
100 | raise "missing host (%h) in :metric_format"
101 | end
102 |
103 | graphs_new = []
104 | @rawconfig[:graphs].each do |name, config|
105 | graphs_new << Graph.new(name, config.merge(@global_config))
106 | end
107 |
108 | dashboards_new = []
109 | @rawconfig[:dashboards].each do |name, config|
110 | dashboards_new << Dashboard.new(name, config.merge(@global_config))
111 | end
112 |
113 | hosts_new = Set.new
114 | clusters_new = Set.new
115 |
116 | # generate host and cluster information at init time
117 | graphs_new.each do |g|
118 | hosts, clusters = g.hosts_clusters
119 | hosts.each { |h| hosts_new << h }
120 | clusters.each { |h| clusters_new << h }
121 | end
122 |
123 | @dashboards, @graphs = dashboards_new, graphs_new
124 | @hosts, @clusters = hosts_new, clusters_new
125 | end
126 | end # Pencil::Config
127 | end # Pencil
128 |
--------------------------------------------------------------------------------
/lib/pencil/config.ru:
--------------------------------------------------------------------------------
1 | require 'pencil'
2 | run Pencil::App
3 |
--------------------------------------------------------------------------------
/lib/pencil/helpers.rb:
--------------------------------------------------------------------------------
1 | module Pencil::Helpers
2 | include Pencil::Models
3 |
4 | @@prefs = [["Start Time", "start"],
5 | ["Duration", "duration"],
6 | ["Width", "width"],
7 | ["Height", "height"]]
8 |
9 | # convert keys to symbols before lookup
10 | def param_lookup(name)
11 | sym_hash = {}
12 | session.each { |k,v| sym_hash[k.to_sym] = v unless v.empty? }
13 | params.each { |k,v| sym_hash[k.to_sym] = v unless v.empty? }
14 | settings.config.global_config[:url_opts].merge(sym_hash)[name.to_sym]
15 | end
16 |
17 | def cluster_graph(g, cluster, title="wtf")
18 | image_url = \
19 | @dash.render_cluster_graph(g, cluster,
20 | :title => title,
21 | :dynamic_url_opts => merge_opts)
22 | zoom_url = cluster_graph_link(@dash, g, cluster)
23 | return image_url, zoom_url
24 | end
25 |
26 | def cluster_graph_link(dash, g, cluster)
27 | link = dash.graph_opts[g]["click"] ||
28 | "/dash/#{cluster}/#{dash.name}/#{g.name}"
29 | return append_query_string(link)
30 | end
31 |
32 | def cluster_zoom_graph(g, cluster, host, title)
33 | image_url = g.render_url([host.name], [cluster], :title => title,
34 | :dynamic_url_opts => merge_opts)
35 | zoom_url = cluster_zoom_link(cluster, host)
36 | return image_url, zoom_url
37 | end
38 |
39 | def cluster_zoom_link(cluster, host)
40 | return append_query_string("/host/#{cluster}/#{host}")
41 | end
42 |
43 | def suggest_cluster_links(clusters, g)
44 | links = []
45 | clusters.each do |c|
46 | href = append_query_string("/dash/#{c}/#{params[:dashboard]}/#{g.name}")
47 | links << "#{c}"
48 | end
49 | return "zoom (" + links.join(", ") + ")"
50 | end
51 |
52 | def suggest_dashboards_links(host, graph)
53 | suggested = suggest_dashboards(host, graph)
54 | return "" if suggested.length == 0
55 |
56 | links = []
57 | suggested.each do |d|
58 | links << "" +
59 | "#{d}"
60 | end
61 | return "(" + links.join(", ") + ")"
62 | end
63 |
64 | # it's mildly annoying that when this set is empty there're no uplinks
65 | # consider adding a link up to the cluster (which is best we can do)
66 | def suggest_dashboards(host, graph)
67 | ret = Set.new
68 |
69 | host.graphs.each do |g|
70 | Dashboard.find_by_graph(g).each do |d|
71 | valid, _ = d.get_valid_hosts(g, host['cluster'])
72 | ret << d.name if valid.member?(host)
73 | end
74 | end
75 |
76 | return ret
77 | end
78 |
79 | # generate the input box fields, filled in to current parameters if specified
80 | def input_boxes
81 | @prefs = @@prefs
82 | erb :'partials/input_boxes', :layout => false
83 | end
84 |
85 | def dash_link(dash, cluster)
86 | return append_query_string("/dash/#{cluster}/#{dash.name}")
87 | end
88 |
89 | def cluster_link(cluster)
90 | return append_query_string("/dash/#{cluster}")
91 | end
92 |
93 | def css_url
94 | style = File.join(settings.root, "public/style.css")
95 | mtime = File.mtime(style).to_i.to_s
96 | return \
97 | %Q[]
98 | end
99 |
100 | def refresh
101 | if settings.config.global_config[:refresh_rate] != false && nowish
102 | rate = settings.config.global_config[:refresh_rate] || 60
103 | return %Q[]
104 | end
105 | end
106 |
107 | def hosts_selector(hosts, print_clusters=false)
108 | @print_clusters = print_clusters
109 | @hosts = hosts
110 | erb :'partials/hosts_selector', :layout => false
111 | end
112 |
113 | def append_query_string(str)
114 | v = str.dup
115 | # CGI.escapeHTML()?
116 | unless request.query_string.empty?
117 | v << "?#{request.query_string.gsub("&", "&")}"
118 | end
119 | return v
120 | end
121 |
122 | def merge_opts
123 | static_opts = ["cluster", "dashboard", "zoom", "host", "session_id"]
124 | opts = params.dup
125 | session.merge(opts).delete_if { |k,v| static_opts.member?(k) || v.empty? }
126 | end
127 |
128 | def shortcuts(str)
129 | @str = str
130 | erb :'partials/shortcuts', :layout => false
131 | end
132 | def cluster_switcher(clusters)
133 | @clusters = clusters
134 | erb :'partials/cluster_switcher', :layout => false
135 | end
136 |
137 | def dash_switcher
138 | erb :'partials/dash_switcher', :layout => false
139 | end
140 |
141 | def graph_switcher
142 | erb :'partials/graph_switcher', :layout => false
143 | end
144 |
145 | def cluster_selector
146 | @clusters = settings.config.clusters.sort + ["global"]
147 | erb :'partials/cluster_selector', :layout => false
148 | end
149 |
150 | def host_uplink
151 | link = "/dash/#{append_query_string(@host.cluster)}"
152 | "zoom out: #{@host.cluster}"
153 | end
154 |
155 | def graph_uplink
156 | link = append_query_string(request.path.split("/")[0..-2].join("/"))
157 | "zoom out: #{@dash}"
158 | end
159 |
160 | def dash_uplink
161 | link = append_query_string(request.path.split("/")[0..-2].join("/"))
162 | "zoom out: #{@params[:cluster]}"
163 | end
164 |
165 | # fixme this is a hack
166 | def header(str)
167 | <<-FOO
168 |
3 | <%= @clusters.sort.collect do |c|
4 | sub = append_query_string(request.path.sub(@cluster, c))
5 | @cluster == c ? "#{c}" : "#{c}"
6 | end.join(" ") %>
7 | <%= if @cluster == "global"
8 | " (global)"
9 | else
10 | # this sub works because the cluster should be first
11 | " (global)"
12 | end %>
13 |
3 |
4 | <%= @dashboards.select do |d|
5 | @cluster == "global" || d.clusters.member?(@cluster)
6 | end.sort.collect do |d|
7 | # this works unless you have a cluster with the same name as a dashboard
8 | sub = append_query_string(request.path.sub(@dash.name, d.name))
9 | @dash == d ? "#{d}" : "#{d}"
10 | end.join(" ") %>
11 |
12 |