Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit 61834cadbcbac6f1d3ec390c44e040271f9bf3ec Andrew Thompson committed
Showing with 527 additions and 0 deletions.
  1. +29 −0 README.markdown
  2. +288 −0 Rakefile
  3. +210 −0 src/rrdtool.erl
29 README.markdown
@@ -0,0 +1,29 @@
+About
+=====
+
+Erlang-rrdtool is a simple module to allow rrdtool to be treated like an erlang
+port via its 'remote control' mode (`rrdtool -`).
+
+Usage
+=====
+
+Creating a rrd (example #1 from the rrdcreate manpage):
+
+<pre>
+1> {ok, Pid} = rrdtool:start().
+{ok,<0.221.0>}
+2> rrdtool:create(Pid, "temperature.rrd", [{"temp", 'GAUGE', [600, -273, 5000]}],
+ [{'AVERAGE', 0.5, 1, 1200}, {'MIN', 0.5, 12, 2400}, {'MAX', 0.5, 12, 2400},
+ {'AVERAGE', 0.5, 12, 2400}]).
+ok
+</pre>
+
+Updating a RRD:
+
+<pre>
+3> rrdtool:update(Pid, "temperature.rrd", [{"temp", 50}]).
+ok
+4> rrdtool:update(Pid, "temperature.rrd", [{"temp", 75}], now()).
+ok
+</pre>
+
288 Rakefile
@@ -0,0 +1,288 @@
+require 'rake/clean'
+
+Dir['tasks/**/*.rake'].each { |rake| load rake }
+
+def percent_to_color(per)
+ if ENV['COLORTERM'].to_s.downcase == 'yes' or ENV['TERM'] =~ /-color$/
+ if per >= 90.0
+ colorstart = "\e[1;32m"
+ elsif per >= 75.0
+ colorstart = "\e[0;32m"
+ elsif per >= 50.0
+ colorstart = "\e[0;33m"
+ elsif per >= 25.0
+ colorstart = "\e[0;31m"
+ else
+ colorstart = "\e[1;31m"
+ end
+ return [colorstart, "\e[0m"]
+ else
+ return ["", ""]
+ end
+end
+
+
+INCLUDE = "include"
+
+vertest = `erl -noshell -eval 'io:format("~n~s~n", [erlang:system_info(otp_release)]).' -s erlang halt | tail -n 1`.chomp
+if vertest =~ /(R\d\d[AB])/
+ OTPVERSION = $1
+else
+ STDERR.puts "unable to determine OTP version! (I got #{vertest})"
+ exit -1
+end
+ERLC_FLAGS = "-I#{INCLUDE} -D #{OTPVERSION} +warn_unused_vars +warn_unused_import +warn_exported_vars +warn_untyped_record"
+
+SRC = FileList['src/*.erl']
+HEADERS = FileList['include/*.hrl']
+OBJ = SRC.pathmap("%{src,ebin}X.beam")
+CONTRIB = FileList['contrib/*']
+DEBUGOBJ = SRC.pathmap("%{src,debug_ebin}X.beam")
+COVERAGE = SRC.pathmap("%{src,coverage}X.txt")
+RELEASE = FileList['src/*.rel.src'].pathmap("%{src,ebin}X")
+
+# check to see if gmake is available, if not fall back on the system make
+if res = `which gmake` and $?.exitstatus.zero? and not res =~ /no gmake in/
+ MAKE = File.basename(res.chomp)
+else
+ MAKE = 'make'
+end
+
+@maxwidth = SRC.map{|x| File.basename(x, 'erl').length}.max
+
+CLEAN.include("ebin/*.beam")
+CLEAN.include("ebin/*.app")
+CLEAN.include("ebin/*.script")
+CLEAN.include("ebin/*.boot")
+CLEAN.include("ebin/*.rel")
+CLEAN.include("debug_ebin/*.beam")
+CLEAN.include("debug_ebin/*.app")
+CLEAN.include("debug_ebin/*.script")
+CLEAN.include("debug_ebin/*.boot")
+CLEAN.include("coverage/*.txt")
+CLEAN.include("coverage/*.txt.failed")
+CLEAN.include("coverage/*.html")
+CLEAN.include("doc/*.html")
+
+verbose(true) unless ENV['quiet']
+
+directory 'ebin'
+directory 'debug_ebin'
+directory 'coverage'
+#directory 'doc'
+
+rule ".beam" => ["%{ebin,src}X.erl"] + HEADERS do |t|
+ sh "erlc -pa ebin -W #{ERLC_FLAGS} +warn_missing_spec -o ebin #{t.source} "
+end
+
+rule ".beam" => ["%{debug_ebin,src}X.erl"] + HEADERS do |t|
+ sh "erlc +debug_info -D EUNIT -pa debug_ebin -W #{ERLC_FLAGS} -o debug_ebin #{t.source} "
+end
+
+rule ".rel" => ["%{ebin,src}X.rel.src"] do |t|
+ contents = File.read(t.source)
+ #p contents
+ while contents =~ /^[\s\t]*([-a-zA-Z0-9_]+),[\s\t]*$/
+ app = $1
+ if app == "erts"
+ version = `erl -noshell -eval 'io:format("~n~s~n", [erlang:system_info(version)]).' -s erlang halt | tail -n 1`.chomp
+ else
+ version = `erl -noshell -eval 'application:load(#{app}), io:format("~n~s~n", [proplists:get_value(#{app}, lists:map(fun({Name, Desc, Vsn}) -> {Name, Vsn} end, application:loaded_applications()))]).' -s erlang halt | tail -n 1`.chomp
+ end
+ if md = /(\d+\.\d+(\.\d+(\.\d+|)|))/.match(version)
+ contents.sub!(app, "{#{app}, \"#{md[1]}\"}")
+ else
+ STDERR.puts "Cannot find application #{app} mentioned in release file!"
+ exit 1
+ end
+ end
+ File.open(t.name, 'w') do |f|
+ f.puts contents
+ end
+end
+
+rule ".txt" => ["%{coverage,debug_ebin}X.beam"] do |t|
+ mod = File.basename(t.source, '.beam')
+ if ENV['modules'] and not ENV['modules'].split(',').include? mod
+ puts "skipping tests for #{mod}"
+ next
+ end
+
+ print " #{mod.ljust(@maxwidth - 1)} : "
+ STDOUT.flush
+ test_output = `erl -noshell -pa debug_ebin -pa contrib/mochiweb/ebin -sname testpx -eval ' cover:start(), cover:compile_beam("#{t.source}"), try eunit:test(#{mod}, [verbose]) of _Any -> cover:analyse_to_file(#{mod}, "coverage/#{mod}.txt"), cover:analyse_to_file(#{mod}, "coverage/#{mod}.html", [html]) catch _:_ -> io:format("This module does not provide a test() function~n"), ok end.' -s erlang halt`
+ if /(All \d+ tests (successful|passed)|There were no tests to run|This module does not provide a test\(\) function|Test (successful|passed))/ =~ test_output
+ File.delete(t.to_s+'.failed') if File.exists?(t.to_s+'.failed')
+ if ENV['verbose']
+ puts test_output.split("\n")[1..-1].map{|x| x.include?('1>') ? x.gsub(/\([a-zA-Z0-9\-@]+\)1>/, '') : x}.join("\n")
+ else
+ out = $1
+ if /(All \d+ tests (successful|passed)|Test (successful|passed))/ =~ test_output
+ colorstart, colorend = percent_to_color(80)
+ #elsif /This module does not provide a test\(\) function/ =~ test_output
+ #colorstart, colorend = percent_to_color(50)
+ else
+ colorstart, colorend = percent_to_color(50)
+ #colorstart, colorend = ["", ""]
+ end
+ puts "#{colorstart}#{out}#{colorend}"
+ #puts " #{mod.ljust(@maxwidth - 1)} : #{out}"
+ end
+ else
+ puts "\e[1;35mFAILED\e[0m"
+ puts test_output.split("\n")[1..-1].map{|x| x.include?('1>') ? x.gsub(/\([a-zA-Z0-9\-@]+\)1>/, '') : x}.join("\n")
+ puts " #{mod.ljust(@maxwidth - 1)} : \e[1;35mFAILED\e[0m"
+ File.delete(t.to_s) if File.exists?(t.to_s)
+ File.new(t.to_s+'.failed', 'w').close
+ end
+end
+
+task :compile => [:contrib, 'ebin'] + HEADERS + OBJ + RELEASE do
+ Dir["ebin/*.rel"].each do |rel|
+ rel = File.basename(rel, '.rel')
+ sh "erl -noshell -eval 'systools:make_script(\"ebin/#{rel}\", [{outdir, \"ebin\"}]).' -s erlang halt -pa ebin"
+ end
+end
+
+task :contrib do
+ CONTRIB.each do |cont|
+ if File.exists? File.join(cont, 'Makefile')
+ sh "#{MAKE} -C #{cont}"
+ elsif File.exists? File.join(cont, 'Rakefile')
+ pwd = Dir.pwd
+ Dir.chdir(cont)
+ sh "#{$0}"
+ Dir.chdir(pwd)
+ end
+ end
+ unless Dir["src/*.app"].length.zero?
+ sh "cp src/*.app ebin/"
+ end
+end
+
+task :default => :compile
+
+task :release => :compile
+
+desc "Alias for test:all"
+task :test => "test:all"
+
+desc "Generate Documentation"
+task :doc do
+ sh('mkdir doc') unless File.directory? 'doc'
+ sh("rm -rf doc/*.html && cd doc && erl -noshell -run edoc files ../#{SRC.join(" ../")} -run init stop")
+end
+
+namespace :test do
+ desc "Compile .beam files with -DEUNIT and +debug_info => debug_ebin"
+ task :compile => [:contrib, 'debug_ebin'] + HEADERS + DEBUGOBJ
+
+ task :contrib do
+ CONTRIB.each do |cont|
+ if File.exists? File.join(cont, 'Makefile')
+ sh "#{MAKE} -C #{cont}"
+ elsif File.exists? File.join(cont, 'Rakefile')
+ pwd = Dir.pwd
+ Dir.chdir(cont)
+ sh "#{$0} debug=yes"
+ Dir.chdir(pwd)
+ end
+ end
+ unless Dir["src/*.app"].length.zero?
+ sh "cp src/*.app debug_ebin/"
+ end
+ end
+
+ desc "run eunit tests and output coverage reports"
+ task :all => [:compile, :eunit, :report_coverage]
+
+ desc "run only the eunit tests"
+ task :eunit => [:compile, 'coverage'] + COVERAGE
+
+ desc "rerun any outstanding tests and report the coverage"
+ task :report_coverage => [:eunit, :report_current_coverage]
+
+ desc "report the percentage code coverage of the last test run"
+ task :report_current_coverage do
+ global_total = 0
+ files = (Dir['coverage/*.txt'] + Dir['coverage/*.txt.failed']).sort
+ maxwidth = files.map{|x| x = File.basename(x, '.failed'); File.basename(x, ".txt").length}.max
+ puts "Code coverage:"
+ files.each do |file|
+ if file =~ /\.txt\.failed$/
+ if ENV['COLORTERM'].to_s.downcase == 'yes' or ENV['TERM'] =~ /-color$/
+ puts " #{File.basename(file, ".txt.failed").ljust(maxwidth)} : \e[1;35mFAILED\e[0m"
+ else
+ puts " #{File.basename(file, ".txt.failed").ljust(maxwidth)} : FAILED"
+ end
+ else
+ total = 0
+ tally = 0
+ File.read(file).each do |line|
+ if line =~ /^\s+[1-9][0-9]*\.\./
+ total += 1
+ tally += 1
+ elsif line =~ /^\s+0\.\./ and not line =~ /^-module/
+ total += 1
+ end
+ end
+ per = tally/total.to_f * 100
+ colorstart, colorend = percent_to_color(per)
+ puts " #{File.basename(file, ".txt").ljust(maxwidth)} : #{colorstart}#{sprintf("%.2f%%", (tally/(total.to_f)) * 100)}#{colorend}"
+ global_total += (tally/(total.to_f)) * 100
+ end
+ end
+ colorstart, colorend = percent_to_color(global_total/files.length)
+ puts "Overall coverage: #{colorstart}#{sprintf("%.2f%%", global_total/files.length)}#{colorend}"
+ end
+
+ task :report_missing_specs do
+ unspecced = []
+ ignored = %w{handle_info handle_cast handle_call code_change terminate init}
+ puts "Functions missing specs:"
+ SRC.each do |src|
+ contents = File.read(src)
+ contents.each do |line|
+ if md = /^([a-z_]+)\(.*?\) ->/.match(line) and not ignored.include?(md[1]) and not md[1][-5..-1] == '_test' and not md[1][-6..-1] == '_test_'
+ unless /^-spec\(#{md[1]}\//.match(contents)
+ unspecced << File.basename(src, '.erl') + ':'+ md[1]
+ end
+ end
+ end
+ end
+ puts " "+unspecced.uniq.join("\n ")
+ end
+
+ desc "run the dialyzer"
+ task :dialyzer do
+ print "running dialyzer..."
+ `dialyzer --check_plt`
+ if $?.exitstatus != 0
+ puts 'no PLT'
+ puts "The dialyzer can't find the initial PLT, you can try building one using `rake test:build_plt`. This can take quite some time."
+ exit(1)
+ end
+ STDOUT.flush
+ # Add -DEUNIT=1 here to make dialyzer evaluate the code in the test cases. This generates some spurious warnings so
+ # it's not set normally but it can be very helpful occasionally.
+ dialyzer_flags = ""
+ dialyzer_flags += " -DEUNIT=1" if ENV['dialyzer_debug']
+ dialyzer_flags += " -Wunderspecs" if ENV['dialyzer_underspecced']
+ contribfiles = Dir['contrib**/*.erl'].join(' ')
+ dialyzer_output = `dialyzer -D#{OTPVERSION}=1 #{dialyzer_flags} --src -I include -c #{SRC.join(' ')} #{contribfiles}`
+ #puts dialyzer_output
+ if $?.exitstatus.zero?
+ puts 'ok'
+ else
+ puts 'not ok'
+ puts dialyzer_output
+ end
+ end
+
+ desc "try to create the dialyzer's initial PLT"
+ task :build_plt do
+ out = `which erlc`
+ foo = out.split('/')[0..-3].join('/')+'/lib/erlang/lib'
+ sh "dialyzer --build_plt -r #{foo}/kernel*/ebin #{foo}/stdlib*/ebin #{foo}/mnesia*/ebin #{foo}/crypto*/ebin #{foo}/eunit*/ebin"
+ end
+end
210 src/rrdtool.erl
@@ -0,0 +1,210 @@
+%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.
+%%%
+%%% Redistribution and use in source and binary forms, with or without
+%%% modification, are permitted provided that the following conditions are met:
+%%%
+%%% 1. Redistributions of source code must retain the above copyright notice,
+%%% this list of conditions and the following disclaimer.
+%%% 2. Redistributions in binary form must reproduce the above copyright
+%%% notice, this list of conditions and the following disclaimer in the
+%%% documentation and/or other materials provided with the distribution.
+%%%
+%%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR
+%%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+%%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+%%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+%%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+%%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+%%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+%%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+%%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+%%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+% @doc An erlang module to interface with rrdtool's remote control mode as an
+% erlang port.
+-module(rrdtool).
+
+-behaviour(gen_server).
+
+% public API
+-export([
+ start/0,
+ start/1,
+ start_link/0,
+ start_link/1,
+ create/4,
+ update/3,
+ update/4
+]).
+
+% gen_server callbacks
+-export([init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3
+]).
+
+-define(STORE_TYPES,
+ ['GAUGE', 'COUNTER', 'DERIVE', 'ABSOLUTE', 'COMPUTE']).
+
+% public API
+
+start() ->
+ gen_server:start(?MODULE, ["/usr/bin/rrdtool"], []).
+
+start(RRDTool) when is_list(RRDTool) ->
+ gen_server:start(?MODULE, [RRDTool], []).
+
+start_link() ->
+ gen_server:start_link(?MODULE, ["/usr/bin/rrdtool"], []).
+
+start_link(RRDTool) when is_list(RRDTool) ->
+ gen_server:start_link(?MODULE, [RRDTool], []).
+
+create(Pid, Filename, Datastores, RRAs) ->
+ gen_server:call(Pid, {create, Filename, format_datastores(Datastores), format_archives(RRAs)}, infinity).
+
+update(Pid, Filename, DatastoreValues) ->
+ gen_server:call(Pid, {update, Filename, format_datastore_values(DatastoreValues), n}, infinity).
+
+update(Pid, Filename, DatastoreValues, Time) ->
+ gen_server:call(Pid, {update, Filename, format_datastore_values(DatastoreValues), Time}, infinity).
+
+% gen_server callbacks
+
+%% @hidden
+init([RRDTool]) ->
+ Port = open_port({spawn_executable, RRDTool}, [{line, 1024}, {args, ["-"]}]),
+ {ok, Port}.
+
+%% @hidden
+handle_call({create, Filename, Datastores, RRAs}, _From, Port) ->
+ Command = "create " ++ Filename ++ " " ++ string:join(Datastores, " ") ++ " " ++ string:join(RRAs, " ") ++ "\n",
+ io:format("Command: ~p~n", [lists:flatten(Command)]),
+ port_command(Port, Command),
+ receive
+ {Port, {data, {eol, "OK"++_}}} ->
+ {reply, ok, Port};
+ {Port, {data, {eol, "ERROR:"++Message}}} ->
+ {reply, {error, Message}, Port}
+ end;
+handle_call({update, Filename, {Datastores, Values}, Time}, _From, Port) ->
+ Timestamp = case Time of
+ n ->
+ "N";
+ {Megaseconds, Seconds, _Microseconds} ->
+ integer_to_list(Megaseconds) ++ integer_to_list(Seconds);
+ Other when is_list(Other) ->
+ Other
+ end,
+ Command = ["update ", Filename, " -t ", string:join(Datastores, ":"), " ", Timestamp, ":", string:join(Values, ":"), "\n"],
+ io:format("Command: ~p~n", [lists:flatten(Command)]),
+ port_command(Port, Command),
+ receive
+ {Port, {data, {eol, "OK"++_}}} ->
+ {reply, ok, Port};
+ {Port, {data, {eol, "ERROR:"++Message}}} ->
+ {reply, {error, Message}, Port}
+ end;
+handle_call(Request, _From, State) ->
+ {reply, {unknown_call, Request}, State}.
+
+%% @hidden
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+%% @hidden
+handle_info(Info, State) ->
+ io:format("info: ~p~n", [Info]),
+ {noreply, State}.
+
+%% @hidden
+terminate(_Reason, _State) ->
+ ok.
+
+%% @hidden
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+% internal functions
+
+format_datastores(Datastores) ->
+ format_datastores(Datastores, []).
+
+format_datastores([], Acc) ->
+ lists:reverse(Acc);
+format_datastores([H | T], Acc) ->
+ case H of
+ {Name, DST, Arguments} when is_list(Name), is_atom(DST), is_list(Arguments) ->
+ case re:run(Name, "^[a-zA-Z0-9_]{1,19}$", [{capture, none}]) of
+ nomatch ->
+ throw({error, bad_datastore_name, Name});
+ match ->
+ case lists:member(DST, ?STORE_TYPES) of
+ false ->
+ throw({error, bad_datastore_type, DST});
+ true ->
+ format_datastores(T, [["DS:", Name, ":", atom_to_list(DST), ":", format_arguments(DST, Arguments)] | Acc])
+ end
+ end;
+ _ ->
+ throw({error, bad_datastore, H})
+ end.
+
+format_arguments(DST, Arguments) ->
+ case DST of
+ 'COMPUTE' ->
+ % TODO rpn expression validation
+ Arguments;
+ _ ->
+ case Arguments of
+ [Heartbeat, Min, Max] when is_integer(Heartbeat), is_integer(Min), is_integer(Max) ->
+ io_lib:format("~B:~B:~B", [Heartbeat, Min, Max]);
+ [Heartbeat, undefined, undefined] when is_integer(Heartbeat) ->
+ io_lib:format("~B:U:U", [Heartbeat]);
+ _ ->
+ throw({error, bad_datastore_arguments, Arguments})
+ end
+ end.
+
+format_archives(RRAs) ->
+ format_archives(RRAs, []).
+
+format_archives([], Acc) ->
+ lists:reverse(Acc);
+format_archives([H | T], Acc) ->
+ case H of
+ {CF, Xff, Steps, Rows} when CF =:= 'MAX'; CF =:= 'MIN'; CF =:= 'AVERAGE'; CF =:= 'LAST' ->
+ format_archives(T, [io_lib:format("RRA:~s:~.2f:~B:~B", [CF, Xff, Steps, Rows]) | Acc]);
+ _ ->
+ throw({error, bad_archive, H})
+ end.
+
+format_datastore_values(DSV) ->
+ format_datastore_values(DSV, [], []).
+
+format_datastore_values([], TAcc, Acc) ->
+ {lists:reverse(TAcc), lists:reverse(Acc)};
+format_datastore_values([H | T], TAcc, Acc) ->
+ case H of
+ {Name, Value} ->
+ case re:run(Name, "^[a-zA-Z0-9_]{1,19}$", [{capture, none}]) of
+ nomatch ->
+ throw({error, bad_datastore_name, Name});
+ match ->
+ format_datastore_values(T, [Name | TAcc], [value_to_list(Value) | Acc])
+ end;
+ _ ->
+ throw({error, bad_datastore_value, H})
+ end.
+
+value_to_list(Value) when is_list(Value) ->
+ Value;
+value_to_list(Value) when is_integer(Value) ->
+ integer_to_list(Value);
+value_to_list(Value) when is_float(Value) ->
+ float_to_list(Value);
+value_to_list(Value) when is_binary(Value) ->
+ binary_to_list(Value).

0 comments on commit 61834ca

Please sign in to comment.
Something went wrong with that request. Please try again.