Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Supporting basic CRUD operations (minus Delete).

  • Loading branch information...
commit 8e87e384a8938f9264534d1acd24ff3f0b43218d 1 parent 8555973
James Edward Gray II authored
104 lib/rrdb.rb
View
@@ -21,6 +21,106 @@ def self.config(hash_or_key = nil)
end
end
- config :rrdtool_path => (run_command("which rrdtool") || "rrdtool").strip,
- :reserve_fields => 10
+ config :rrdtool_path => ( run_command("which rrdtool") ||
+ "rrdtool" ).strip,
+ :database_directory => ".",
+ :reserve_fields => 10,
+ :data_sources => "GAUGE:600:U:U",
+ :round_robin_archives => Array.new
+
+ def initialize(id)
+ @id = id
+ end
+
+ attr_reader :id
+
+ def path
+ File.join(self.class.config[:database_directory], "#{id}.rrd")
+ end
+
+ def fields
+ rrdtool(:info).to_s.scan(/^ds\[([^\]]+)\]/).flatten.uniq
+ end
+
+ def step
+ (rrdtool(:info).to_s[/^step\s+=\s+(\d+)/, 1] || 300).to_i
+ end
+
+ def update(time, data)
+ data = Hash[*data.map { |f, v| [f.to_s, v] }.flatten]
+ if File.exist? path
+ claim_new_fields(data.keys)
+ else
+ create_database(time, data.keys)
+ end
+ rrdtool(:update, "'#{time.to_i}:#{fields.map { |f| data[f].send(data[f].to_s =~ /\A\d+\./ ? :to_f : :to_i) }.join(':')}'")
+ end
+
+ def fetch(field, range = Hash.new)
+ params = "'#{field}' "
+ %w[start end resolution].each do |option|
+ if param = range[option.to_sym] || range[option]
+ params << " --#{option} '#{param.to_i}'"
+ end
+ end
+ data = rrdtool(:fetch, params)
+ fields = data.to_a.first.split
+ results = Hash.new
+ data.scan(/^\s*(\d+):((?:\s+\S+){#{fields.size}})/) do |time, values|
+ floats = values.split.map { |f| f =~ /\A\d/ ? Float(f) : 0 }
+ results[Time.at(time.to_i)] = Hash[*fields.zip(floats).flatten]
+ end
+ results
+ end
+
+ private
+
+ def create_database(time, field_names)
+ schema = String.new
+ %w[step start].each do |option|
+ if setting = self.class.config[:"database_#{option}"]
+ schema << " --#{option} '#{setting.to_i}'"
+ elsif option == "start"
+ schema << " --start '#{(time - 10).to_i}'"
+ end
+ end
+ field_names.each do |f|
+ dst = if (setting = self.class.config[:data_sources]).is_a? String
+ setting
+ else
+ setting[f.to_sym] || setting[f]
+ end
+ schema << " 'DS:#{f}:#{dst}'"
+ end
+ (self.class.config[:reserve_fields].to_i - field_names.size).times do |i|
+ schema << " 'DS:_reserved#{i}:GAUGE:600:U:U'"
+ end
+ Array(self.class.config[:round_robin_archives]).each do |a|
+ schema << " 'RRA:#{a}'"
+ end
+ rrdtool(:create, schema.strip)
+ end
+
+ def claim_new_fields(field_names)
+ old_fields = fields
+ new_fields = field_names - old_fields
+ unless new_fields.empty?
+ reserved = old_fields.grep(/\A_reserved\d+\Z/).
+ sort_by { |f| f[/\d+/].to_i }
+ if new_fields.size > reserved.size
+
+ else
+ claims = new_fields.zip(reserved).
+ map { |n, o| " --data-source-rename '#{o}:#{n}'"}.
+ join.strip
+ rrdtool(:tune, claims)
+ end
+ end
+ end
+
+ def rrdtool(command, params = nil)
+ self.class.run_command(
+ "#{self.class.config[:rrdtool_path]} #{command} '#{path}' #{params}"
+ )
+ end
end
37 test/rrd_manager.rb
View
@@ -0,0 +1,37 @@
+#!/usr/bin/env ruby -wKU
+
+module RRDManager
+ def setup
+ save_config
+ modify_config_for_tests
+ create_a_new_database
+ end
+
+ def teardown
+ cleanup_rrd_files
+ restore_config
+ end
+
+ def save_config
+ @saved_config = RRDB.config.dup
+ end
+
+ def modify_config_for_tests
+ RRDB.config( :database_directory => File.dirname(__FILE__),
+ :round_robin_archives => "AVERAGE:0.5:1:24" )
+ end
+
+ def restore_config
+ RRDB.config.replace(@saved_config)
+ end
+
+ def create_a_new_database
+ @db = RRDB.new(rand(10_000))
+ end
+
+ def cleanup_rrd_files
+ Dir.glob("#{RRDB.config[:database_directory]}/*.rrd") do |rrd|
+ File.unlink(rrd)
+ end
+ end
+end
9 test/tc_config.rb
View
@@ -3,15 +3,10 @@
require "test/unit"
require "rrdb"
+require File.join(File.dirname(__FILE__), "rrd_manager")
class TestConfig < Test::Unit::TestCase
- def setup
- @saved_config = RRDB.config.dup
- end
-
- def teardown
- RRDB.config.replace(@saved_config)
- end
+ include RRDManager
def test_config_is_a_hash_with_defaults
assert_instance_of(Hash, RRDB.config)
41 test/tc_create_database.rb
View
@@ -0,0 +1,41 @@
+#!/usr/bin/env ruby -wKU
+
+require "test/unit"
+
+require "rrdb"
+require File.join(File.dirname(__FILE__), "rrd_manager")
+
+class TestCreateDatabase < Test::Unit::TestCase
+ include RRDManager
+
+ def test_database_is_not_created_with_instance
+ assert(!File.exist?(@db.path), "Database existed before update.")
+ end
+
+ def test_database_is_created_at_first_update
+ @db.update(Time.now, :some_field => 10)
+ assert(File.exist?(@db.path), "Database wasn't created at update time.")
+ end
+
+ def test_fields_in_initial_update_are_in_the_created_database
+ @db.update(Time.now, :a => 1, :b => 2, :c => 3)
+ %w[a b c].each do |field|
+ assert(@db.fields.include?(field), "Field not in created database.")
+ end
+ end
+
+ def test_extra_fields_are_reserved_in_the_created_database_by_config
+ RRDB.config(:reserve_fields => 3)
+ @db.update(Time.now, :a => 1)
+ 2.times do |i|
+ assert( @db.fields.include?("_reserved#{i}"),
+ "Reserved field not in created database." )
+ end
+ end
+
+ def test_step_can_be_customized_for_creation
+ RRDB.config(:database_step => 1_000)
+ @db.update(Time.now, :a => 1)
+ assert_equal(1_000, @db.step)
+ end
+end
50 test/tc_fetch.rb
View
@@ -0,0 +1,50 @@
+#!/usr/bin/env ruby -wKU
+
+require "test/unit"
+
+require "rrdb"
+require File.join(File.dirname(__FILE__), "rrd_manager")
+
+class TestFetch < Test::Unit::TestCase
+ include RRDManager
+
+ def test_example_data_from_tutorial
+ RRDB.config( :reserve_fields => 0,
+ :database_start => Time.at(920804400),
+ :data_sources => {:speed => "COUNTER:600:U:U"},
+ :round_robin_archives => %w[ AVERAGE:0.5:1:24
+ AVERAGE:0.5:6:10 ] )
+
+ %w[ 920804700:12345 920805000:12357 920805300:12363
+ 920805600:12363 920805900:12363 920806200:12373
+ 920806500:12383 920806800:12393 920807100:12399
+ 920807400:12405 920807700:12411 920808000:12415
+ 920808300:12420 920808600:12422 920808900:12423 ].each do |data_point|
+ time, value = data_point.split(":")
+ @db.update(Time.at(time.to_i), :speed => value.to_i)
+ end
+
+ results = @db.fetch( :AVERAGE, :start => Time.at(920804400),
+ :end => Time.at(920809200) )
+ %w[ 920804700:0
+ 920805000:4.0000000000e-02
+ 920805300:2.0000000000e-02
+ 920805600:0.0000000000e+00
+ 920805900:0.0000000000e+00
+ 920806200:3.3333333333e-02
+ 920806500:3.3333333333e-02
+ 920806800:3.3333333333e-02
+ 920807100:2.0000000000e-02
+ 920807400:2.0000000000e-02
+ 920807700:2.0000000000e-02
+ 920808000:1.3333333333e-02
+ 920808300:1.6666666667e-02
+ 920808600:6.6666666667e-03
+ 920808900:3.3333333333e-03
+ 920809200:0 ].each do |expected|
+ time, value = expected.split(":")
+ assert_in_delta( Float(value), results[Time.at(time.to_i)]["speed"],
+ 2 ** -20 )
+ end
+ end
+end
21 test/tc_update.rb
View
@@ -0,0 +1,21 @@
+#!/usr/bin/env ruby -wKU
+
+require "test/unit"
+
+require "rrdb"
+require File.join(File.dirname(__FILE__), "rrd_manager")
+
+class TestUpdate < Test::Unit::TestCase
+ include RRDManager
+
+ def test_update_claims_new_fields_as_needed
+ @db.update(Time.now, :a => 1)
+ %w[b c].each do |field|
+ assert(!@db.fields.include?(field), "Unused field in database.")
+ end
+ @db.update(Time.now, :b => 2, :c => 3)
+ %w[b c].each do |field|
+ assert(@db.fields.include?(field), "Field not claimed in database.")
+ end
+ end
+end
3  test/ts_all.rb
View
@@ -5,3 +5,6 @@
require "tc_version"
require "tc_run_command"
require "tc_config"
+require "tc_create_database"
+require "tc_update"
+require "tc_fetch"
Please sign in to comment.
Something went wrong with that request. Please try again.