├── .gitignore ├── COPYING ├── Gemfile ├── README.md ├── Rakefile ├── bin └── check_graph.rb ├── graphite_graph.gemspec ├── lib ├── graphite_graph.rb └── graphite_graph │ └── version.rb └── samples ├── basic ├── basic-field.graph ├── basic-field.png ├── basic.graph ├── basic.png └── example.png ├── group ├── README.md ├── group.graph └── group.png ├── holt-winters ├── aberration-with-thresholds.graph ├── aberration-with-thresholds.png ├── basic-with-aberration-on-y.graph ├── basic-with-aberration-on-y.png ├── basic-with-aberration.graph ├── basic-with-aberration.png ├── basic.graph ├── basic.png ├── custom-colors.graph ├── custom-colors.png ├── just-aberration.graph └── just-aberration.png ├── lines ├── lines.graph └── lines.png ├── monitoring ├── README.md ├── monitor.graph └── monitor.png └── thresholds ├── thresholds.graph └── thresholds.png /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | Gemfile.lock 3 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2010, 2011 R.I.Pienaar 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What? 2 | ===== 3 | 4 | Small DSL in progress to describe a graphite graph. 5 | 6 | For full information please see the wiki pages on the 7 | project home at GitHub 8 | 9 | Who? 10 | ==== 11 | 12 | R.I.Pienaar / http://devco.net/ / @ripienaar / rip@devco.net 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | -------------------------------------------------------------------------------- /bin/check_graph.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'json' 5 | require 'graphite_graph' 6 | require 'net/http' 7 | require 'uri' 8 | require 'optparse' 9 | require 'pp' 10 | 11 | crits = [] 12 | warns = [] 13 | check_data = {} 14 | url = "http://localhost/render/?" 15 | graph = nil 16 | check_number = 3 17 | 18 | opt = OptionParser.new 19 | 20 | opt.on("--graphite [URL]", "Base URL for the Graphite installation") do |v| 21 | url = v 22 | end 23 | 24 | opt.on("--graph [GRAPH]", "Graph defintition") do |v| 25 | graph = v 26 | end 27 | 28 | opt.on("--warning [WARN]", "Warning threshold, can be specified multiple times") do |v| 29 | warns << Float(v) 30 | end 31 | 32 | opt.on("--critical [CRITICAL]", "Critical threshold, can be specified multiple times") do |v| 33 | crits << Float(v) 34 | end 35 | 36 | opt.on("--check [NUM]", Integer, "Number of past data items to check") do |v| 37 | check_number = v 38 | end 39 | 40 | opt.parse! 41 | 42 | def status_exit(msg, code) 43 | puts msg 44 | exit code 45 | end 46 | 47 | unless (graph && File.exist?(graph)) 48 | status_exit "UNKNOWN - Can't find graph defintion #{graph}", 3 49 | end 50 | 51 | def check_data(data, min, max) 52 | fails = [] 53 | 54 | data.keys.each do |target| 55 | data["#{target}"].compact! 56 | if min == max # we got just one value to compare against 57 | if min < 0 58 | # if the threshold is < 0 we check for values below the threshold but have no way to say that 59 | # critical / warning is above -0.5 for example unless you specify a 2 value band 60 | if (data["#{target}"].min <= min) 61 | fails << {:target => target, :item => data["#{target}"].min, :operator => "<=", :expected => min} 62 | end 63 | else 64 | if (data["#{target}"].max >= max) 65 | fails << {:target => target, :item => data["#{target}"].max, :operator => ">=", :expected => max} 66 | end 67 | end 68 | else # we have a range of values to compare against and the values must be between 69 | if (data["#{target}"].min <= min) 70 | fails << {:target => target, :item => data["#{target}"].min, :operator => "<=", :expected => min} 71 | end 72 | 73 | if (data["#{target}"].max >= max) 74 | fails << {:target => target, :item => data["#{target}"].max, :operator => ">=", :expected => max} 75 | end 76 | end 77 | end 78 | 79 | fails.empty? ? false : fails 80 | end 81 | 82 | def print_and_exit(results, code) 83 | exitcodes = ["OK", "WARNING", "CRITICAL", "UNKNOWN"] 84 | 85 | msg = results.map do |r| 86 | "%s %s %s %s" % [r[:target], r[:item], r[:operator], r[:expected]] 87 | end.join(", ") 88 | 89 | status_exit "%s - %s" % [exitcodes[code], msg], code 90 | end 91 | 92 | 93 | graphite = GraphiteGraph.new(graph) 94 | 95 | uri = URI.parse("%s?%s" % [ url, graphite.url(:json) ]) 96 | 97 | json = Net::HTTP.get_response(uri) 98 | 99 | status_exit("UNKNOWN - Could not request graph data for HTTP code #{json.code}", 3) unless json.code == "200" 100 | 101 | data = JSON.load(json.body) 102 | 103 | data.each do |d| 104 | unless d["target"] =~ /(warn|crit)_[01]$/ 105 | check_data[ d["target"] ] = d["datapoints"].last(check_number).map{|i| i.first} 106 | end 107 | end 108 | 109 | crits = graphite.critical_threshold if crits.empty? and graphite.critical_threshold 110 | warns = graphite.warning_threshold if warns.empty? and graphite.warning_threshold 111 | 112 | if crits.empty? || warns.empty? || check_data.empty? 113 | status_exit "UNKNOWN: Graph does not have Data, Warning and Critical information", 3 114 | end 115 | 116 | if results = check_data(check_data, crits.min, crits.max) 117 | print_and_exit results, 2 118 | 119 | elsif results = check_data(check_data, warns.min, warns.max) 120 | print_and_exit results, 1 121 | 122 | else 123 | status_exit "#{graph.split('/').last.split('.').first} OK - #{check_data}", 0 124 | end 125 | -------------------------------------------------------------------------------- /graphite_graph.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "graphite_graph/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "graphite_graph" 7 | s.version = GraphiteGraph::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["R.I.Pienaar", "Tom Taylor"] 10 | s.email = ["rip@devco.net", "tom@tomtaylor.co.uk"] 11 | s.homepage = "https://github.com/ripienaar/graphite-graph-dsl" 12 | s.summary = %q{DSL for generating Graphite graphs} 13 | 14 | s.rubyforge_project = "graphite_graph" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 18 | s.require_paths = ["lib"] 19 | end 20 | -------------------------------------------------------------------------------- /lib/graphite_graph.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'cgi' 3 | require "graphite_graph/version" 4 | # A small DSL to assist in the creation of Graphite graphs 5 | # see https://github.com/ripienaar/graphite-graph-dsl/wiki 6 | # for full details 7 | class GraphiteGraph 8 | attr_reader :info, :properties, :targets, :target_order, :critical_threshold, :warning_threshold 9 | 10 | def initialize(file, overrides={}, info={}) 11 | @info = info 12 | @file = file 13 | @munin_mode = false 14 | @overrides = overrides 15 | @linecount = 0 16 | 17 | @critical_threshold = [] 18 | @warning_threshold = [] 19 | 20 | load_graph 21 | end 22 | 23 | def defaults 24 | @properties = {:title => nil, 25 | :vtitle => nil, 26 | :vtitle_right => nil, 27 | :width => 500, 28 | :height => 250, 29 | :graphtype => nil, 30 | :from => "-1hour", 31 | :until => "now", 32 | :surpress => false, 33 | :description => nil, 34 | :hide_axes => nil, 35 | :hide_legend => nil, 36 | :hide_grid => nil, 37 | :hide_y_axis => nil, 38 | :graph_only => nil, 39 | :ymin => nil, 40 | :yminleft => nil, 41 | :yminright => nil, 42 | :ymax => nil, 43 | :ymaxleft => nil, 44 | :ymaxright => nil, 45 | :yunit_system => nil, 46 | :linewidth => nil, 47 | :linemode => nil, 48 | :fontsize => nil, 49 | :fontbold => false, 50 | :fontname => nil, 51 | :timezone => nil, 52 | :xformat => nil, 53 | :background_color => nil, 54 | :foreground_color => nil, 55 | :draw_null_as_zero => false, 56 | :major_grid_line_color => nil, 57 | :minor_grid_line_color => nil, 58 | :area => :none, 59 | :logbase => nil, 60 | :placeholders => nil, 61 | :area_alpha => nil, 62 | :theme => nil, 63 | :unique_legend => nil}.merge(@overrides) 64 | end 65 | 66 | def [](key) 67 | if key == :url 68 | url 69 | else 70 | @properties[key] 71 | end 72 | end 73 | 74 | def method_missing(meth, *args) 75 | if properties.include?(meth) 76 | properties[meth] = args.first unless @overrides.include?(meth) 77 | else 78 | super 79 | end 80 | end 81 | 82 | def load_graph 83 | @properties = defaults 84 | @targets = {} 85 | @target_order = [] 86 | 87 | self.instance_eval(File.read(@file)) unless @file == :none 88 | end 89 | 90 | def service(service, data, &blk) 91 | raise "No hostname given for this instance" unless info[:hostname] 92 | 93 | @service_mode = {:service => service, :data => data} 94 | 95 | blk.call 96 | 97 | @service_mode = false 98 | end 99 | 100 | # add forecast, bands, aberrations and actual fields using the 101 | # Holt-Winters Confidence Band prediction model 102 | # 103 | # hw_predict :foo, :data => "some.data.item", :alias => "Some Item" 104 | # 105 | # You can tweak the colors by setting: 106 | # :forecast_color => "blue" 107 | # :bands_color => "grey" 108 | # :aberration_color => "red" 109 | # 110 | # You can add an aberration line: 111 | # 112 | # :aberration_line => true, 113 | # :aberration_second_y => true 114 | # 115 | # You can disable the forecast line by setting: 116 | # 117 | # :forecast_line => false 118 | # 119 | # You can disable the confidence lines by settings: 120 | # 121 | # :bands_lines => false 122 | # 123 | # You can disable the display of the actual data: 124 | # 125 | # :actual_line => false 126 | def hw_predict(name, args) 127 | raise ":data is needed as an argument to a Holt-Winters Confidence forecast" unless args[:data] 128 | 129 | unless args[:forecast_line] == false 130 | forecast_args = args.clone 131 | forecast_args[:data] = "holtWintersForecast(#{forecast_args[:data]})" 132 | forecast_args[:alias] = "#{args[:alias]} Forecast" 133 | forecast_args[:color] = args[:forecast_color] || "blue" 134 | field "#{name}_forecast", forecast_args 135 | end 136 | 137 | unless args[:bands_lines] == false 138 | bands_args = args.clone 139 | bands_args[:data] = "holtWintersConfidenceBands(#{bands_args[:data]})" 140 | bands_args[:color] = args[:bands_color] || "grey" 141 | bands_args[:dashed] = true 142 | bands_args[:alias] = "#{args[:alias]} Confidence" 143 | field "#{name}_bands", bands_args 144 | end 145 | 146 | if args[:aberration_line] 147 | aberration_args = args.clone 148 | aberration_args[:data] = "holtWintersAberration(keepLastValue(#{aberration_args[:data]}))" 149 | aberration_args[:color] = args[:aberration_color] || "orange" 150 | aberration_args[:alias] = "#{args[:alias]} Aberration" 151 | aberration_args[:second_y_axis] = true if aberration_args[:aberration_second_y] 152 | field "#{name}_aberration", aberration_args 153 | end 154 | 155 | if args[:critical] 156 | color = args[:critical_color] || "red" 157 | critical :value => args[:critical], :color => color, :name => name 158 | end 159 | 160 | if args[:warning] 161 | color = args[:warning_color] || "orange" 162 | warning :value => args[:warning], :color => color, :name => name 163 | end 164 | 165 | args[:color] ||= "yellow" 166 | 167 | field name, args unless args[:actual_line] == false 168 | end 169 | 170 | alias :forecast :hw_predict 171 | 172 | # takes a series of metrics in a wildcard query and aggregates the values by a subgroup 173 | # 174 | # data must contain a wildcard query, a subgroup position, and an optional aggregate function. 175 | # if the aggregate function is omitted, sumSeries will be used. 176 | # 177 | # group :data => "metric.*.value", :subgroup => "2", :aggregator => "sumSeries" 178 | # 179 | def group(name, args) 180 | raise ":data is needed as an argument to group metrics" unless args[:data] 181 | raise ":subgroup is needed as an argument to group metrics" unless args.include?(:subgroup) 182 | 183 | args[:aggregator] = "sumSeries" unless args[:aggregator] 184 | 185 | group_args = args.clone 186 | group_args[:data] = "groupByNode(#{group_args[:data]},#{group_args[:subgroup]},\"#{group_args[:aggregator]}\")" 187 | field "#{name}_group", group_args 188 | 189 | end 190 | 191 | # draws a single dashed line with predictable names, defaults to red line 192 | # 193 | # data can be a single item or a 2 item array, it doesn't break if you supply 194 | # more but # more than 2 items just doesn't make sense generally 195 | # 196 | # critical :value => [700, -700], :color => "red" 197 | # 198 | # You can prevent the line from being drawn but just store the ranges for monitoring 199 | # purposes by adding :hide => true to the arguments 200 | def critical(options) 201 | raise "critical lines need a value" unless options[:value] 202 | 203 | @critical_threshold = [options[:value]].flatten 204 | 205 | options[:color] ||= "red" 206 | 207 | unless options[:hide] 208 | @critical_threshold.each_with_index do |crit, index| 209 | line :caption => "crit_#{index}", :value => crit, :color => options[:color], :dashed => true 210 | end 211 | end 212 | end 213 | 214 | # draws a single dashed line with predictable names, defaults to orange line 215 | # 216 | # data can be a single item or a 2 item array, it doesn't break if you supply 217 | # more but # more than 2 items just doesn't make sense generally 218 | # 219 | # warning :value => [700, -700], :color => "orange" 220 | # 221 | # You can prevent the line from being drawn but just store the ranges for monitoring 222 | # purposes by adding :hide => true to the arguments 223 | def warning(options) 224 | raise "warning lines need a value" unless options[:value] 225 | 226 | @warning_threshold = [options[:value]].flatten 227 | 228 | options[:color] ||= "orange" 229 | 230 | unless options[:hide] 231 | @warning_threshold.flatten.each_with_index do |warn, index| 232 | line :caption => "warn_#{index}", :value => warn, :color => options[:color], :dashed => true 233 | end 234 | end 235 | end 236 | 237 | # draws a simple line on the graph with a caption, value and color. 238 | # 239 | # line :caption => "warning", :value => 50, :color => "orange" 240 | def line(options) 241 | raise "lines need a caption" unless options.include?(:caption) 242 | raise "lines need a value" unless options.include?(:value) 243 | raise "lines need a color" unless options.include?(:color) 244 | 245 | options[:alias] = options[:caption] unless options[:alias] 246 | 247 | args = {:data => "threshold(#{options[:value]})", :color => options[:color], :alias => options[:alias]} 248 | 249 | args[:dashed] = true if options[:dashed] 250 | args[:second_y_axis] = true if options[:second_y_axis] 251 | 252 | field "line_#{@linecount}", args 253 | 254 | @linecount += 1 255 | end 256 | 257 | # adds a field to the graph, each field needs a unique name 258 | def field(name, args) 259 | raise "A field called #{name} already exist for this graph" if targets.include?(name) 260 | 261 | default = {} 262 | 263 | if @service_mode 264 | default[:data] = [info[:hostname], @service_mode[:service], @service_mode[:data], name].join(".") 265 | end 266 | 267 | targets[name] = default.merge(args) 268 | target_order << name 269 | end 270 | 271 | def url(format = nil, url=true) 272 | return nil if properties[:surpress] 273 | 274 | url_parts = [] 275 | 276 | dual_axis = false 277 | targets.each_value { |v| dual_axis = true if v.include?(:second_y_axis) } 278 | 279 | [:title, :vtitle, :from, :width, :height, :until].each do |item| 280 | url_parts << "#{item}=#{properties[item]}" if properties[item] 281 | end 282 | 283 | url_parts << "areaMode=#{properties[:area]}" if properties[:area] 284 | url_parts << "hideAxes=#{properties[:hide_axes]}" if properties[:hide_axes] 285 | url_parts << "hideLegend=#{properties[:hide_legend]}" unless properties[:hide_legend].nil? 286 | url_parts << "graphOnly=#{properties[:graph_only]}" if properties[:graph_only] 287 | url_parts << "hideGrid=#{properties[:hide_grid]}" if properties[:hide_grid] 288 | url_parts << "hideYAxis=#{properties[:hide_y_axis]}" if properties[:hide_y_axis] 289 | if dual_axis 290 | url_parts << "yMinLeft=#{properties[:ymin]}" if properties[:ymin] 291 | url_parts << "yMaxLeft=#{properties[:ymax]}" if properties[:ymax] 292 | else 293 | url_parts << "yMin=#{properties[:ymin]}" if properties[:ymin] 294 | url_parts << "yMax=#{properties[:ymax]}" if properties[:ymax] 295 | end 296 | url_parts << "yMinLeft=#{properties[:yminleft]}" if properties[:yminleft] 297 | url_parts << "yMaxLeft=#{properties[:ymaxleft]}" if properties[:ymaxleft] 298 | url_parts << "yMinRight=#{properties[:yminright]}" if properties[:yminright] 299 | url_parts << "yMaxRight=#{properties[:ymaxright]}" if properties[:ymaxright] 300 | url_parts << "yUnitSystem=#{properties[:yunit_system]}" if properties[:yunit_system] 301 | url_parts << "lineWidth=#{properties[:linewidth]}" if properties[:linewidth] 302 | url_parts << "lineMode=#{properties[:linemode]}" if properties[:linemode] 303 | url_parts << "fontSize=#{properties[:fontsize]}" if properties[:fontsize] 304 | url_parts << "fontBold=#{properties[:fontbold]}" if properties[:fontbold] 305 | url_parts << "fontName=#{properties[:fontname]}" if properties[:fontname] 306 | url_parts << "drawNullAsZero=#{properties[:draw_null_as_zero]}" if properties[:draw_null_as_zero] 307 | url_parts << "tz=#{properties[:timezone]}" if properties[:timezone] 308 | url_parts << "xFormat=#{properties[:xformat]}" if properties[:xformat] 309 | url_parts << "majorGridLineColor=#{properties[:major_grid_line_color]}" if properties[:major_grid_line_color] 310 | url_parts << "minorGridLineColor=#{properties[:minor_grid_line_color]}" if properties[:minor_grid_line_color] 311 | url_parts << "graphType=#{properties[:graphtype]}" if properties[:graphtype] 312 | url_parts << "bgcolor=#{properties[:background_color]}" if properties[:background_color] 313 | url_parts << "fgcolor=#{properties[:foreground_color]}" if properties[:foreground_color] 314 | url_parts << "vtitleRight=#{properties[:vtitle_right]}" if properties[:vtitle_right] 315 | url_parts << "logBase=#{properties[:logbase]}" if properties[:logbase] 316 | url_parts << "areaAlpha=#{properties[:area_alpha]}" if properties[:area_alpha] 317 | url_parts << "minXStep=#{properties[:min_x_step]}" if properties[:min_x_step] 318 | url_parts << "uniqueLegend=#{properties[:unique_legend]}" if properties[:unique_legend] 319 | url_parts << "template=#{properties[:theme]}" if properties[:theme] 320 | 321 | target_order.each do |name| 322 | target = targets[name] 323 | 324 | if target[:target] 325 | url_parts << "target=#{target[:target]}" 326 | else 327 | raise "field #{name} does not have any data associated with it" unless target[:data] 328 | 329 | graphite_target = target[:data] 330 | graphite_target = "transformNull(#{graphite_target},#{target[:transform_null]})" if target[:transform_null] 331 | graphite_target = "lowestAverage(#{graphite_target},#{target[:lowest_average]})" if target[:lowest_average] 332 | graphite_target = "highestAverage(#{graphite_target},#{target[:highest_average]})" if target[:highest_average] 333 | graphite_target = "averageAbove(#{graphite_target},#{target[:average_above]})" if target[:average_above] 334 | graphite_target = "averageBelow(#{graphite_target},#{target[:average_below]})" if target[:average_below] 335 | graphite_target = "removeAbovePercentile(#{graphite_target},#{target[:remove_above_percentile]})" if target[:remove_above_percentile] 336 | graphite_target = "removeAboveValue(#{graphite_target},#{target[:remove_above_value]})" if target[:remove_above_value] 337 | graphite_target = "removeBelowPercentile(#{graphite_target},#{target[:remove_below_percentile]})" if target[:remove_below_percentile] 338 | graphite_target = "removeBelowValue(#{graphite_target},#{target[:remove_below_value]})" if target[:remove_below_value] 339 | graphite_target = "lineWidth(#{graphite_target},#{target[:field_linewidth]})" if target[:field_linewidth] 340 | graphite_target = "keepLastValue(#{graphite_target})" if target[:keep_last_value] 341 | if target[:derivative] 342 | graphite_target = "derivative(#{graphite_target})" 343 | elsif target[:non_negative_derivative] 344 | graphite_target = "nonNegativeDerivative(#{graphite_target})" 345 | end 346 | graphite_target = "sum(#{graphite_target})" if target[:sum] 347 | graphite_target = "sumSeriesWithWildcard(#{graphite_target},#{target[:sum_with_wildcard]})" if target[:sum_with_wildcard] 348 | graphite_target = "highestAverage(#{graphite_target},#{target[:highest_average]})" if target[:highest_average] 349 | graphite_target = "scale(#{graphite_target},#{target[:scale]})" if target[:scale] 350 | graphite_target = "scaleToSeconds(#{graphite_target},#{target[:scale_to_seconds]})" if target[:scale_to_seconds] 351 | if target[:as_percent] == true 352 | graphite_target = "asPercent(#{graphite_target})" 353 | elsif target[:as_percent] 354 | graphite_target = "asPercent(#{graphite_target},#{target[:as_percent]})" 355 | end 356 | graphite_target = "summarize(#{graphite_target},\"#{target[:summarize]}\")" if target[:summarize] 357 | graphite_target = "drawAsInfinite(#{graphite_target})" if target[:line] 358 | graphite_target = "movingAverage(#{graphite_target},#{target[:smoothing]})" if target[:smoothing] 359 | 360 | graphite_target = "color(#{graphite_target},\"#{target[:color]}\")" if target[:color] 361 | graphite_target = "dashed(#{graphite_target})" if target[:dashed] 362 | graphite_target = "secondYAxis(#{graphite_target})" if target[:second_y_axis] 363 | 364 | unless target.include?(:subgroup) 365 | if target[:alias_by_node] 366 | graphite_target = "aliasByNode(#{graphite_target},#{target[:alias_by_node]})" 367 | elsif target[:alias_sub_search] 368 | graphite_target = "aliasSub(#{graphite_target},\"#{target[:alias_sub_search]}\",\"#{target[:alias_sub_replace]}\")" 369 | elsif target[:alias] 370 | graphite_target = "alias(#{graphite_target},\"#{target[:alias]}\")" 371 | elsif target[:no_alias] 372 | graphite_target = graphite_target # no-op 373 | else 374 | graphite_target = "alias(#{graphite_target},\"#{name.to_s.capitalize}\")" 375 | end 376 | 377 | if target[:cacti_style] 378 | graphite_target = "cactiStyle(#{graphite_target})" 379 | elsif target[:legend_value] 380 | graphite_target = "legendValue(#{graphite_target},\"#{target[:legend_value]}\")" 381 | end 382 | end 383 | 384 | url_parts << "target=#{graphite_target}" 385 | end 386 | end 387 | 388 | url_parts << "format=#{format}" if format 389 | 390 | if url 391 | properties[:placeholders].each { |k,v| url_parts.each {|part| part.gsub!("%{#{k}}", v.to_s) } } if properties[:placeholders].is_a?(Hash) 392 | url_str = url_parts.map { |pair| k,v = pair.split('='); "#{k}=#{CGI.escape(v)}" }.join("&") 393 | 394 | url_str 395 | else 396 | url_parts 397 | end 398 | end 399 | end 400 | -------------------------------------------------------------------------------- /lib/graphite_graph/version.rb: -------------------------------------------------------------------------------- 1 | class GraphiteGraph 2 | VERSION = "0.0.8" 3 | end 4 | -------------------------------------------------------------------------------- /samples/basic/basic-field.graph: -------------------------------------------------------------------------------- 1 | title "Test Graph" 2 | 3 | field :iowait, :data => "sumSeries(example.munin.load.load)", 4 | :color => "red", 5 | :alias => "Load Average", 6 | :derivative => true 7 | -------------------------------------------------------------------------------- /samples/basic/basic-field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/basic/basic-field.png -------------------------------------------------------------------------------- /samples/basic/basic.graph: -------------------------------------------------------------------------------- 1 | # graph settings 2 | title "Basic Graph" 3 | vtitle "percent" 4 | area :stacked 5 | 6 | # individual fields 7 | field :iowait, :scale => 0.001, 8 | :color => "red", 9 | :alias => "IO Wait", 10 | :data => "sumSeries(derivative(mw*.munin.cpu.iowait))" 11 | -------------------------------------------------------------------------------- /samples/basic/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/basic/basic.png -------------------------------------------------------------------------------- /samples/basic/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/basic/example.png -------------------------------------------------------------------------------- /samples/group/README.md: -------------------------------------------------------------------------------- 1 | This functionality requires Graphite 1.0 to be 2 | compiled. The current stable release does not have the 3 | function groupByNode. This feature will be available 4 | when Graphite 1.0 is released. 5 | 6 | Graphite 1.0 Functions 7 | [http://graphite.readthedocs.org/en/1.0/functions.html] 8 | 9 | The callback aggregator is a function will be applied 10 | to the result of groups returned. In this example the 11 | group will be the hard disk name (Ex: sda1). The 12 | function "sumSeries" will be applied to each result 13 | adding the total read iops of all like named disks 14 | and graphing the result. 15 | 16 | -------------------------------------------------------------------------------- /samples/group/group.graph: -------------------------------------------------------------------------------- 1 | title "CollectD - Read IOPS" 2 | vtitle "iops" 3 | description "CollectD Total IOPS by Disk" 4 | from "-4hour" 5 | linemode "staircase" 6 | 7 | group :iops, :data => 'collectd.*.disk.sd*1.disk_ops.read', 8 | :subgroup => 3, 9 | :aggregator => "sumSeries" 10 | -------------------------------------------------------------------------------- /samples/group/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/group/group.png -------------------------------------------------------------------------------- /samples/holt-winters/aberration-with-thresholds.graph: -------------------------------------------------------------------------------- 1 | title "Male Site Users - Abereation" 2 | vtitle "users" 3 | width 800 4 | height 500 5 | from "-12hours" 6 | area :none 7 | description "Male users on the site" 8 | hide_legend true 9 | 10 | forecast :male, :data => "sumSeries(*.site.users.male)", 11 | :alias => "Male", 12 | :aberration_line => true, 13 | :forecast_line => false, 14 | :bands_lines => false, 15 | :actual_line => false, 16 | :critical => [700, -700], 17 | :warning => [300, -300], 18 | :aberration_color => "blue" 19 | -------------------------------------------------------------------------------- /samples/holt-winters/aberration-with-thresholds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/holt-winters/aberration-with-thresholds.png -------------------------------------------------------------------------------- /samples/holt-winters/basic-with-aberration-on-y.graph: -------------------------------------------------------------------------------- 1 | title "Male Site Users - Aberration" 2 | vtitle "users" 3 | width 800 4 | height 500 5 | from "-12hours" 6 | area :none 7 | description "Male users on the site" 8 | hide_legend true 9 | 10 | forecast :male, :data => "sumSeries(*.site.users.male)", 11 | :alias => "Male", 12 | :aberration_line => true, 13 | :aberration_second_y => true 14 | -------------------------------------------------------------------------------- /samples/holt-winters/basic-with-aberration-on-y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/holt-winters/basic-with-aberration-on-y.png -------------------------------------------------------------------------------- /samples/holt-winters/basic-with-aberration.graph: -------------------------------------------------------------------------------- 1 | title "Male Site Users - Aberration" 2 | vtitle "users" 3 | width 800 4 | height 500 5 | from "-12hours" 6 | area :none 7 | description "Male users on the site" 8 | hide_legend true 9 | 10 | forecast :male, :data => "sumSeries(*.site.users.male)", 11 | :alias => "Male", 12 | :aberration_line => true 13 | -------------------------------------------------------------------------------- /samples/holt-winters/basic-with-aberration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/holt-winters/basic-with-aberration.png -------------------------------------------------------------------------------- /samples/holt-winters/basic.graph: -------------------------------------------------------------------------------- 1 | title "Male Site Users" 2 | vtitle "users" 3 | width 800 4 | height 500 5 | from "-12hours" 6 | area :none 7 | description "Male users on the site" 8 | hide_legend true 9 | 10 | forecast :male, :data => "sumSeries(*.site.users.male)", 11 | :alias => "Male" 12 | -------------------------------------------------------------------------------- /samples/holt-winters/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/holt-winters/basic.png -------------------------------------------------------------------------------- /samples/holt-winters/custom-colors.graph: -------------------------------------------------------------------------------- 1 | title "Male Site Users" 2 | vtitle "users" 3 | width 800 4 | height 500 5 | from "-12hours" 6 | area :none 7 | description "Male users on the site" 8 | hide_legend true 9 | 10 | forecast :male, :data => "sumSeries(*.site.users.male)", 11 | :alias => "Male", 12 | :forecast_color => "yellow", 13 | :bands_color => "white", 14 | :color => "green" 15 | -------------------------------------------------------------------------------- /samples/holt-winters/custom-colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/holt-winters/custom-colors.png -------------------------------------------------------------------------------- /samples/holt-winters/just-aberration.graph: -------------------------------------------------------------------------------- 1 | title "Male Site Users - Aberration" 2 | vtitle "users" 3 | width 800 4 | height 500 5 | from "-12hours" 6 | area :none 7 | description "Male users on the site" 8 | hide_legend true 9 | 10 | forecast :male, :data => "sumSeries(*.site.users.male)", 11 | :alias => "Male", 12 | :aberration_line => true, 13 | :forecast_line => false, 14 | :bands_lines => false, 15 | :actual_line => false 16 | -------------------------------------------------------------------------------- /samples/holt-winters/just-aberration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/holt-winters/just-aberration.png -------------------------------------------------------------------------------- /samples/lines/lines.graph: -------------------------------------------------------------------------------- 1 | title "Horizontal Lines" 2 | width 400 3 | height 250 4 | ymin 0 5 | ymax 125 6 | 7 | line :caption => "Critical", :value => 100, :color => "red" 8 | line :caption => "Warning", :value => 50, :color => "orange", :dashed => true 9 | -------------------------------------------------------------------------------- /samples/lines/lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/lines/lines.png -------------------------------------------------------------------------------- /samples/monitoring/README.md: -------------------------------------------------------------------------------- 1 | Given a graph with critical and warning data on it 2 | this Nagios compatible check will fetch the JSON data 3 | of the graph and make sure no data is outside of the 4 | warning and critical levels. 5 | 6 | title "Down Network" 7 | hide_legend true 8 | 9 | forecast :down, :data => "keepLastValue(my_net.munin.if_eth0.down)", 10 | :alias => "Down", 11 | :aberration_line => true, 12 | :forecast_line => false, 13 | :bands_lines => false, 14 | :actual_line => false, 15 | :aberration_color => "blue" 16 | 17 | # record the thresholds but do not draw them 18 | critical :value => [700, -700], :hide => true 19 | warning :value => [300, -300], :hide => true 20 | 21 | This is a Holt Winters prediction graph of network data received 22 | by a host, it has acceptable threshold for how aberrant the data 23 | may be - if the prediction says the data is too far out of range 24 | an alert will be raised. 25 | 26 | % check_graph.rb --graph monitor.graph --graphite "http://graphite.your.net/render/" 27 | CRITICAL - Down Aberration 1623 > 700 28 | -------------------------------------------------------------------------------- /samples/monitoring/monitor.graph: -------------------------------------------------------------------------------- 1 | title "Load Average" 2 | hide_legend true 3 | 4 | field :iowait, :data => "keepLastValue(example.munin.load.load)", 5 | :color => "red", 6 | :alias => "Load Average" 7 | 8 | critical :value => 0.3 9 | warning :value => 0.1 10 | -------------------------------------------------------------------------------- /samples/monitoring/monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/monitoring/monitor.png -------------------------------------------------------------------------------- /samples/thresholds/thresholds.graph: -------------------------------------------------------------------------------- 1 | title "Threshold Lines" 2 | width 400 3 | height 250 4 | ymin -130 5 | ymax 130 6 | hide_legend true 7 | 8 | warning :value => [50, -50], :color => "orange" 9 | critical :value => [100, -100], :color => "red" 10 | 11 | -------------------------------------------------------------------------------- /samples/thresholds/thresholds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ripienaar/graphite-graph-dsl/48047a39f1bf17922b4392d6aa4bb0bd9718daf2/samples/thresholds/thresholds.png --------------------------------------------------------------------------------