├── Gemfile ├── Gemfile.lock ├── README.md ├── charcoal.rb ├── config.ru ├── config.yml ├── config.yml.example ├── public └── css │ └── style.css ├── slug.rb └── views ├── layout.haml └── stats.haml /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gem 'sinatra' 3 | gem 'haml' 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | haml (3.0.25) 5 | rack (1.2.2) 6 | sinatra (1.2.2) 7 | rack (~> 1.1) 8 | tilt (>= 1.2.2, < 2.0) 9 | tilt (1.2.2) 10 | 11 | PLATFORMS 12 | ruby 13 | 14 | DEPENDENCIES 15 | haml 16 | sinatra 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Charcoal: Simple Graphite Templates 2 | 3 | ## What it is 4 | 5 | Charcoal is a simple [Sinatra]() app designed to allow you to quickly put together a "dashboard" 6 | of system status images from [Graphite](), or any similar service that 7 | is capable of generating images directly from a URL (like the one in the 8 | examples, [Placekitten]()). 9 | 10 | [Sinatra]: http://www.sinatrarb.com/ 11 | [Graphite]: http://graphite.wikidot.com/ 12 | [Placekitten]: http://placekitten.com/ 13 | 14 | ## How to use it 15 | 16 | ### Installation 17 | 18 | It's a Sinatra app ready to deploy right to 19 | [Heroku](http://www.heroku.com). Just `heroku create; git push heroku 20 | master` to lock and load. 21 | 22 | If you want to run it somewhere else, just make sure you've installed 23 | the `sinatra` and `haml` gems. 24 | 25 | ### Configuration 26 | 27 | The project includes a `config.yml.example` file, which you should 28 | rename to `config.yml`. The format is documented in the config file 29 | itself. 30 | -------------------------------------------------------------------------------- /charcoal.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'sinatra' 3 | require 'haml' 4 | require 'yaml' 5 | require File.join(File.expand_path(File.dirname(__FILE__)), "slug.rb") 6 | 7 | get '/all' do 8 | @data = YAML.load_file "config.yml" 9 | puts @data 10 | haml :stats, :locals => {:data => @data} 11 | end 12 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require './charcoal' 2 | run Sinatra::Application 3 | 4 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # Charcoal supports the following structure: 2 | # - A page has multiple Projects. 3 | # - Projects can have several Sections. 4 | # - Sections have Targets, Attributes and Images 5 | # - Each target is a Graphite command for what to draw 6 | # - The Attributes section gives global attributes for HOW to draw it 7 | # - Each Image shows a title and other local attributes (such as time) 8 | # for how to draw the end image. 9 | # 10 | # ----------------------------------------------------------- 11 | # 12 | # The format of the config file is as follows: 13 | # 14 | # Title: Page title 15 | # RenderURL: http://graphite.you.com/URL/to/Rendering/? 16 | # [Project Name]: 17 | # [Section Name]: 18 | # Targets: 19 | # [Name]: [Command] 20 | # [Name]: [Command] 21 | # [Name]: [Command] 22 | # Attributes: 23 | # [Attribute]: [Value] 24 | # [Attribute]: [Value] 25 | # [Attribute]: [Value] 26 | # Images: 27 | # [ImageName]: 28 | # [Attribute]: [Value] 29 | # [Attribute]: [Value] 30 | # [ImageName]: 31 | # [Attribute]: [Value] 32 | # [Attribute]: [Value] 33 | # 34 | # [Project Name]: 35 | # ... 36 | # 37 | # (Repeat ad infinitum.) 38 | # 39 | # ----------------------------------------------------------- 40 | # 41 | # Image Sizes 42 | # 43 | # There's a constant gutter of 20px between each image if 44 | # there are multiple images on a line. 45 | # 46 | # Projects have 920px of "image width." This means you can 47 | # have: 48 | # - a single image 920px wide on a line 49 | # - two images that are each 450px wide (with 20px btwthem) 50 | # - 3 across at 293/294/293px 51 | # - and so on. 52 | # 53 | # Sections have 70px of "image width." This means, most 54 | # commonly, two 370px wide images on a line. 55 | 56 | Title: LifeChurch.tv Dashboard 57 | RenderURL: http://placekitten.com/370/220/? 58 | 59 | YouVersion: 60 | App Server Load: 61 | Targets: 62 | Rate: alias(sumSeries(collectd.app*.load.load),"Load") 63 | Attributes: 64 | width: 293 65 | height: 220 66 | bgcolor: FFFFFF 67 | fgcolor: 111111 68 | Images: 69 | 5 Minutes: 70 | from: -6minutes 71 | 1 Hour: 72 | from: -1hours 73 | 1 Day: 74 | from: -1days 75 | 1 Month: 76 | from: -1months 77 | Database Load: 78 | Targets: 79 | Rate: alias(sumSeries(collectd.db*.load.load),"Load") 80 | Attributes: 81 | width: 293 82 | height: 220 83 | bgcolor: FFFFFF 84 | fgcolor: 111111 85 | Images: 86 | 5 Minutes: 87 | from: -6minutes 88 | 1 Hour: 89 | from: -1hours 90 | 1 Day: 91 | from: -1days 92 | 1 Month: 93 | from: -1months 94 | 95 | MySite.Com: 96 | Load: 97 | Targets: 98 | Rate: alias(sumSeries(collectd.app*.load.load),"Load") 99 | Attributes: 100 | width: 293 101 | height: 220 102 | bgcolor: FFFFFF 103 | fgcolor: 111111 104 | Images: 105 | 5 Minutes: 106 | from: -6minutes 107 | 1 Hour: 108 | from: -1hours 109 | 1 Day: 110 | from: -1days 111 | 1 Month: 112 | from: -1months 113 | Memory: 114 | Targets: 115 | Rate: alias(sumSeries(collectd.db*.load.load),"Load") 116 | Attributes: 117 | width: 293 118 | height: 220 119 | bgcolor: FFFFFF 120 | fgcolor: 111111 121 | Images: 122 | 5 Minutes: 123 | from: -6minutes 124 | 1 Hour: 125 | from: -1hours 126 | 1 Day: 127 | from: -1days 128 | 1 Month: 129 | from: -1months 130 | -------------------------------------------------------------------------------- /config.yml.example: -------------------------------------------------------------------------------- 1 | # Charcoal supports the following structure: 2 | # - A page has multiple Projects. 3 | # - Projects can have several Sections. 4 | # - Sections have Targets, Attributes and Images 5 | # - Each target is a Graphite command for what to draw 6 | # - The Attributes section gives global attributes for HOW to draw it 7 | # - Each Image shows a title and other local attributes (such as time) 8 | # for how to draw the end image. 9 | # 10 | # ----------------------------------------------------------- 11 | # 12 | # The format of the config file is as follows: 13 | # 14 | # Title: Page title 15 | # RenderURL: http://graphite.you.com/URL/to/Rendering/? 16 | # [Project Name]: 17 | # [Section Name]: 18 | # Targets: 19 | # [Name]: [Command] 20 | # [Name]: [Command] 21 | # [Name]: [Command] 22 | # Attributes: 23 | # [Attribute]: [Value] 24 | # [Attribute]: [Value] 25 | # [Attribute]: [Value] 26 | # Images: 27 | # [ImageName]: 28 | # [Attribute]: [Value] 29 | # [Attribute]: [Value] 30 | # [ImageName]: 31 | # [Attribute]: [Value] 32 | # [Attribute]: [Value] 33 | # 34 | # [Project Name]: 35 | # ... 36 | # 37 | # (Repeat ad infinitum.) 38 | # 39 | # ----------------------------------------------------------- 40 | # 41 | # Image Sizes 42 | # 43 | # There's a constant gutter of 20px between each image if 44 | # there are multiple images on a line. 45 | # 46 | # Projects have 920px of "image width." This means you can 47 | # have: 48 | # - a single image 920px wide on a line 49 | # - two images that are each 450px wide (with 20px btwthem) 50 | # - 3 across at 293/294/293px 51 | # - and so on. 52 | # 53 | # Sections have 70px of "image width." This means, most 54 | # commonly, two 370px wide images on a line. 55 | 56 | Title: LifeChurch.tv Dashboard 57 | RenderURL: http://placekitten.com/370/220/? 58 | 59 | YouVersion: 60 | App Server Load: 61 | Targets: 62 | Rate: alias(sumSeries(collectd.app*.load.load),"Load") 63 | Attributes: 64 | width: 293 65 | height: 220 66 | bgcolor: FFFFFF 67 | fgcolor: 111111 68 | Images: 69 | 5 Minutes: 70 | from: -6minutes 71 | 1 Hour: 72 | from: -1hours 73 | 1 Day: 74 | from: -1days 75 | 1 Month: 76 | from: -1months 77 | Database Load: 78 | Targets: 79 | Rate: alias(sumSeries(collectd.db*.load.load),"Load") 80 | Attributes: 81 | width: 293 82 | height: 220 83 | bgcolor: FFFFFF 84 | fgcolor: 111111 85 | Images: 86 | 5 Minutes: 87 | from: -6minutes 88 | 1 Hour: 89 | from: -1hours 90 | 1 Day: 91 | from: -1days 92 | 1 Month: 93 | from: -1months 94 | 95 | MySite.Com: 96 | Load: 97 | Targets: 98 | Rate: alias(sumSeries(collectd.app*.load.load),"Load") 99 | Attributes: 100 | width: 293 101 | height: 220 102 | bgcolor: FFFFFF 103 | fgcolor: 111111 104 | Images: 105 | 5 Minutes: 106 | from: -6minutes 107 | 1 Hour: 108 | from: -1hours 109 | 1 Day: 110 | from: -1days 111 | 1 Month: 112 | from: -1months 113 | Memory: 114 | Targets: 115 | Rate: alias(sumSeries(collectd.db*.load.load),"Load") 116 | Attributes: 117 | width: 293 118 | height: 220 119 | bgcolor: FFFFFF 120 | fgcolor: 111111 121 | Images: 122 | 5 Minutes: 123 | from: -6minutes 124 | 1 Hour: 125 | from: -1hours 126 | 1 Day: 127 | from: -1days 128 | 1 Month: 129 | from: -1months 130 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /* CSS Reset --------------------------- */ 2 | 3 | html, body, div, span, applet, object, iframe, 4 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 5 | a, abbr, acronym, address, big, cite, code, 6 | del, dfn, em, img, ins, kbd, q, s, samp, 7 | small, strike, strong, sub, sup, tt, var, 8 | b, u, i, center, 9 | dl, dt, dd, ol, ul, li, 10 | fieldset, form, label, legend, 11 | table, caption, tbody, tfoot, thead, tr, th, td, 12 | article, aside, canvas, details, embed, 13 | figure, figcaption, footer, header, hgroup, 14 | menu, nav, output, ruby, section, summary, 15 | time, mark, audio, video { 16 | margin: 0; 17 | padding: 0; 18 | border: 0; 19 | font-size: 100%; 20 | font: inherit; 21 | vertical-align: baseline; 22 | } 23 | 24 | strong { font-weight: bold; } 25 | 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | 51 | /* ----------------------------------- */ 52 | 53 | body { 54 | background-color: #eee; 55 | font-family: "Helvetica Neue", HelveticaNeue, Helvetica, sans-serif; 56 | } 57 | 58 | h1 { 59 | font-size: 36px; 60 | font-weight: bold; 61 | text-align: center; 62 | margin-bottom: 10px; 63 | } 64 | 65 | div.links { 66 | padding-left: 20px; 67 | } 68 | 69 | h2 { 70 | font-size: 30px; 71 | font-weight; bold; 72 | text-align: center; 73 | padding-bottom: 10px; 74 | margin-bottom: 10px; 75 | } 76 | 77 | h3 { 78 | float: left; 79 | width: 119px; 80 | padding: 0 20px 20px 20px; 81 | text-align: right; 82 | font-size: 22px; 83 | font-weight: bold; 84 | margin-top: 40px; 85 | } 86 | 87 | h4 { 88 | font-size: 16px; 89 | margin: 10px 0; 90 | } 91 | 92 | section { 93 | background-color: #fff; 94 | border: 1px solid #aaa; 95 | padding: 20px; 96 | padding-left: 0px; 97 | width: 940px; 98 | margin: 20px auto; 99 | } 100 | 101 | div.section { 102 | margin: 20px 0; 103 | border-bottom: 1px solid #aaa; 104 | } 105 | 106 | div.tile { 107 | float: left; 108 | text-align: right; 109 | padding-left: 20px; 110 | margin: 20px 0; 111 | } 112 | 113 | div.graphs { 114 | float: right; 115 | border-left: 1px solid #aaa; 116 | margin-top: 20px; 117 | width: 780px; 118 | } 119 | -------------------------------------------------------------------------------- /slug.rb: -------------------------------------------------------------------------------- 1 | module Slug 2 | def slugify 3 | ret = self.strip 4 | ret.downcase! 5 | 6 | #blow away apostrophes 7 | ret.gsub! /['`]/,"" 8 | 9 | # @ --> at, and & --> and 10 | ret.gsub! /\s*@\s*/, " at " 11 | ret.gsub! /\s*&\s*/, " and " 12 | 13 | #replace all non alphanumeric, underscore or periods with underscore 14 | ret.gsub! /\s*[^A-Za-z0-9\.\-]\s*/, '-' 15 | 16 | #convert double underscores to single 17 | ret.gsub! /_+/,"_" 18 | 19 | #strip off leading/trailing underscore 20 | ret.gsub! /\A[_\.]+|[_\.]+\z/,"" 21 | 22 | return ret 23 | end 24 | end 25 | 26 | class String 27 | include Slug 28 | end 29 | -------------------------------------------------------------------------------- /views/layout.haml: -------------------------------------------------------------------------------- 1 | = yield 2 | -------------------------------------------------------------------------------- /views/stats.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %link{:rel => "stylesheet", :type => "text/css", :href => "css/style.css"} 5 | %title= data['Title'] 6 | %body 7 | %section 8 | %h1= data.delete('Title') 9 | - renderURL = data.delete('RenderURL') 10 | - data.each do |k, v| 11 | %div.links 12 | %strong 13 | %a{:href => "\##{k.slugify}"}>< 14 | = k 15 | \:  16 | - v.each do |kk, vv| 17 | %a{:href => "\##{k.slugify}_#{kk.slugify}"}< 18 | = kk 19 |   20 | - data.each do |k, v| 21 | %section 22 | %h2{:id => k.slugify}= k 23 | -# Graphs 24 | - v.each do |kk, vv| 25 | %div.section 26 | -# Sections 27 | %h3{:id => "#{k.slugify}_#{kk.slugify}"}= kk 28 | .graphs 29 | - vv["Images"].each do |jjj, vvv| 30 | -# Where Graphs Go 31 | .tile 32 | - attributes = "" 33 | - attributes = vv["Targets"].values.join "&target=" 34 | - vv["Attributes"].each {|ak, av| attributes << "&#{ak}=#{av}" } 35 | - vvv.each {|ak, av| attributes << "&#{ak}=#{av}" } 36 | - imgsrc = "#{renderURL}&target=#{attributes}" 37 | %img{:id => "#{k.slugify}_#{kk.slugify}_#{jjj.slugify}_img", :src => imgsrc, :alt => jjj} 38 | %h4{:id => "#{k.slugify}_#{kk.slugify}_#{jjj.slugify}"}= jjj 39 | %div{:style => "clear: both;"} 40 | --------------------------------------------------------------------------------