├── .gitignore ├── README.md ├── apps └── rolf │ ├── include │ └── rolf.hrl │ ├── src │ ├── collectors │ │ ├── rolf_command.erl │ │ ├── rolf_loadtime.erl │ │ └── rolf_munin_node.erl │ ├── rolf.app.src │ ├── rolf.erl │ ├── rolf_app.erl │ ├── rolf_collector.erl │ ├── rolf_collector_sup.erl │ ├── rolf_config_validation.erl │ ├── rolf_plugin.erl │ ├── rolf_recorder.erl │ ├── rolf_recorder_sup.erl │ ├── rolf_rrd.erl │ ├── rolf_service.erl │ └── rolf_util.erl │ └── test │ └── rolf_SUITE.erl ├── bin └── graph.sh ├── doc └── windows.md ├── plugins ├── rebar ├── rebar.config └── rel ├── files ├── app.config ├── erl ├── log4erl.conf ├── nodetool ├── plugins │ ├── disk │ │ ├── disk.config │ │ └── disk.sh │ ├── loadtime │ │ ├── loadtime.config │ │ └── loadtime.sh │ └── uptime │ │ └── uptime.config ├── rolf ├── services.config └── vm.args └── reltool.config /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.sw[nop] 3 | .eunit 4 | .DS_Store 5 | app.config 6 | data 7 | deps 8 | ebin 9 | etc 10 | log 11 | rel/rolf 12 | tags 13 | TEST-*.xml 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rolf 2 | ==== 3 | 4 | 'Azamat is run Big Data analytics on famous last words in devops. Most common is 5 | "I know, I roll own monitoring tool!"' 6 | -- [@DEVOPS_BORAT](http://twitter.com/#!/DEVOPS_BORAT/status/51324117521141760) 7 | 8 | Overview 9 | -------- 10 | 11 | - Monitoring and graphing tool like Munin or collectd. 12 | - Written in Erlang. 13 | - Sample frequency down to 1 second, configured per plug-in. 14 | - Record data on multiple nodes in parallel. 15 | - Asynchronous data gathering. 16 | - HTTP interface for HTML, graphs and data via JSON. 17 | - Writing plug-ins is simple. Plug-ins are kept resident between updates, as in 18 | collectd. 19 | - Runs anywhere Erlang runs (at least Linux, OS X, Windows). 20 | 21 | Getting started 22 | --------------- 23 | 24 | Configure which node(s) should be recorders in etc/app.config. 25 | 26 | [{rolf, [recorders, [rolf@john]]}]. 27 | 28 | Configure which services should run on which nodes in `etc/services.config`. 29 | 30 | {node, rolf@john, [disk, loadtime]}. 31 | {node, rolf@paul, [disk]}. 32 | {node, rolf@george, [disk]}. 33 | {node, rolf@ringo, [disk]}. 34 | 35 | Start Rolf on each machine. 36 | 37 | [user@john ~] rolf start 38 | [user@paul ~] rolf start 39 | [user@george ~] rolf start 40 | [user@ringo ~] rolf start 41 | 42 | Architecture 43 | ------------ 44 | 45 | - An Erlang cluster is created. Each node runs the rolf application. 46 | - One node is designated the recorder by it's config file. 47 | - If the cluster has no recorder, nothing happens. 48 | - If a recorder has been started, service configuration is distributed to all 49 | other nodes by the recorder. 50 | - A node manager process starts services and they start emitting samples. 51 | - Services can be implemented as Erlang functions, external commands or external 52 | daemons. 53 | - Services collect samples and send them to all live recorders. 54 | - Recorders and collectors can be added and removed from the cluster 55 | dynamically. 56 | 57 | The supervision tree of an Rolf node looks like this (node has recorder): 58 | 59 | rolf_sup 60 | ├── rolf_recorder 61 | │ └── errd_server 62 | └── rolf_collector_sup 63 | ├── rolf_service (svc1) 64 | └── rolf_service (svc2) 65 | └── rolf_external 66 | 67 | Plugins 68 | ------- 69 | 70 | Plugins add additional collectors to Rolf. 71 | 72 | - Plugins live in `plugins`. 73 | - A plugin has a config file, see 74 | `plugins/loadtime/loadtime.config` for an example. 75 | - Plugin config can be overridden per-site by customising `services.config`. 76 | - Plugins can use an Erlang module, a command or a daemon to collect data. 77 | - Plugin collectors written in Erlang should implement the `rolf_collector` 78 | behaviour. 79 | - The `collect/1` function should capture data. The argument, `Service`, is a 80 | `service` record, which includes lots of useful info such as name and options 81 | (see `apps/rolf/include/rolf.hrl` for more info). 82 | 83 | Author 84 | ------ 85 | 86 | Ben Godfrey, ben@ben2.com, http://aftnn.org/. 87 | 88 | License 89 | ------- 90 | 91 | Rolf - a monitoring and graphing tool like Munin or collectd. 92 | 93 | Copyright (C) 2011 Ben Godfrey. 94 | 95 | This program is free software: you can redistribute it and/or modify 96 | it under the terms of the GNU General Public License as published by 97 | the Free Software Foundation, either version 3 of the License, or 98 | (at your option) any later version. 99 | 100 | This program is distributed in the hope that it will be useful, 101 | but WITHOUT ANY WARRANTY; without even the implied warranty of 102 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 103 | GNU General Public License for more details. 104 | 105 | You should have received a copy of the GNU General Public License 106 | along with this program. If not, see . 107 | -------------------------------------------------------------------------------- /apps/rolf/include/rolf.hrl: -------------------------------------------------------------------------------- 1 | %% @doc Rolf records. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | %% import eunit macros into all modules 23 | -ifdef(TEST). 24 | -include_lib("eunit/include/eunit.hrl"). 25 | -endif. 26 | 27 | %% @doc State record for Rolf recorder. 28 | -record(recorder, {collectors=[], 29 | rrd=undefined}). 30 | 31 | %% @doc State record for Rolf nodes. 32 | -record(node, {services=[]}). 33 | 34 | %% @doc State record for Rolf services, which contains multiple metrics. 35 | -record(service, {name=undefined, 36 | plugin=undefined, 37 | recorders=undefined, 38 | module=undefined, 39 | command=undefined, 40 | frequency=undefined, 41 | timeout=undefined, 42 | archives=undefined, 43 | graph_title=undefined, 44 | graph_vlabel=undefined, 45 | metrics=undefined, 46 | config=undefined, 47 | tref=undefined}). 48 | 49 | %% @doc Record for a single metric. 50 | -record(metric, {name=undefined, 51 | label="", 52 | type=gauge, 53 | draw=line, 54 | min=undefined, 55 | max=undefined, 56 | colour=undefined}). 57 | 58 | %% @doc Record for Rolf samples. values is a list of tuples of format 59 | %% {Metric, Value}. 60 | -record(sample, {node=undefined, 61 | service=undefined, 62 | values=undefined}). 63 | -------------------------------------------------------------------------------- /apps/rolf/src/collectors/rolf_command.erl: -------------------------------------------------------------------------------- 1 | %% @doc Plugin module for generic commands. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf_command). 23 | -behaviour(rolf_collector). 24 | 25 | %% rolf_collector callbacks 26 | -export([start/1, collect/2, stop/2]). 27 | 28 | -include("rolf.hrl"). 29 | 30 | %% =================================================================== 31 | %% rolf_collector callbacks 32 | %% =================================================================== 33 | 34 | %% @doc Start collector. 35 | start(_Service) -> ok. 36 | 37 | %% @doc Execute the command and return the parsed results. 38 | collect(Service, State) -> 39 | {State, parse_output(Service, os:cmd(Service#service.command))}. 40 | 41 | %% @doc Stop collector. 42 | stop(_Service, _State) -> ok. 43 | 44 | %% =================================================================== 45 | %% Helper functions 46 | %% =================================================================== 47 | 48 | %% @doc Parse output from external command. 49 | parse_output(Service, Output) -> 50 | Values = [parse_line(Line) || Line <- split_lines(Output)], 51 | #sample{node=node(), service=Service, values=Values}. 52 | 53 | %% @doc Split output into lines, drop terminating ".\n" line. 54 | split_lines(Lines) -> 55 | Lines1 = string:tokens(Lines, "\n"), 56 | lists:filter(fun(L) -> L /= "." end, Lines1). 57 | 58 | %% @doc Parse line into {atom, int_or_float} tuple. 59 | parse_line(Line) -> 60 | {K, V} = list_to_tuple(string:tokens(Line, " ")), 61 | {list_to_atom(K), rolf_util:list_to_num(V)}. 62 | 63 | %% =================================================================== 64 | %% Tests 65 | %% =================================================================== 66 | 67 | -ifdef(TEST). 68 | 69 | split_lines_test() -> 70 | Result = split_lines("loadtime 0.99\n.\n"), 71 | ?assertEqual(["loadtime 0.99"], Result). 72 | 73 | parse_line_test() -> 74 | Result = parse_line("loadtime 0.99"), 75 | ?assertEqual({loadtime, 0.99}, Result). 76 | 77 | parse_output_test() -> 78 | Result = parse_output(loadtime, "loadtime 0.99\n.\n"), 79 | ?assertEqual(#sample{node=node(), 80 | service=loadtime, 81 | values=[{loadtime, 0.99}]}, 82 | Result). 83 | 84 | parse_output_many_test() -> 85 | Result = parse_output(loadtime, "loadtime 0.99\nttfb 0.65\nrendertime 2\n.\n"), 86 | ?assertEqual(#sample{node=node(), 87 | service=loadtime, 88 | values=[{loadtime, 0.99}, 89 | {ttfb, 0.65}, 90 | {rendertime, 2}]}, 91 | Result). 92 | 93 | -endif. 94 | -------------------------------------------------------------------------------- /apps/rolf/src/collectors/rolf_loadtime.erl: -------------------------------------------------------------------------------- 1 | %% @doc Plugin module for measuring web site load time. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf_loadtime). 23 | -behaviour(rolf_collector). 24 | 25 | %% rolf_collector callbacks 26 | -export([start/1, collect/2, stop/2]). 27 | 28 | %% helpers 29 | -export([time_url/1]). 30 | 31 | -include("rolf.hrl"). 32 | 33 | %% =================================================================== 34 | %% rolf_collector callbacks 35 | %% =================================================================== 36 | 37 | %% @doc Start collector. 38 | start(_Service) -> 39 | inets:start(). 40 | 41 | %% @doc HTTP load time collector function for Rolf. Options should contain a key 42 | %% urls with value [{Name, Url}]. 43 | collect(Service, State) -> 44 | Config = Service#service.config, 45 | Url = proplists:get_value(url, Config, []), 46 | Values = [{loadtime, time_url(Url)}], 47 | Sample = #sample{node=node(), service=Service, values=Values}, 48 | {State, Sample}. 49 | 50 | %% @doc Stop collector. 51 | stop(_Service, _State) -> 52 | inets:stop(). 53 | 54 | %% =================================================================== 55 | %% Helper functions 56 | %% =================================================================== 57 | 58 | %% @doc Return how long it took to load resource at Url in milliseconds. 59 | time_url(Url) -> 60 | {T, _} = timer:tc(httpc, request, [head, {Url, []}, [], []]), 61 | round(T / 1000). 62 | -------------------------------------------------------------------------------- /apps/rolf/src/collectors/rolf_munin_node.erl: -------------------------------------------------------------------------------- 1 | %% @doc Plugin module for retrieving measurements from a munin node. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf_munin_node). 23 | -behaviour(rolf_collector). 24 | 25 | %% rolf_collector callbacks 26 | -export([start/1, collect/2, stop/2]). 27 | 28 | -include("rolf.hrl"). 29 | 30 | %% =================================================================== 31 | %% rolf_collector callbacks 32 | %% =================================================================== 33 | 34 | %% @doc Start collector. 35 | start(Service) -> 36 | munin_node_params(Service). 37 | 38 | %% @doc HTTP load time collector function for Rolf. Options should contain a key 39 | %% urls with value [{Name, Url}]. 40 | collect(Service, {Host, Port, Opts, Plugin}=State) -> 41 | case gen_tcp:connect(Host, Port, Opts) of 42 | {ok, Sock} -> 43 | case gen_tcp:recv(Sock, 0) of 44 | {ok, _Banner} -> 45 | Values = fetch(Sock, Plugin), 46 | ok = gen_tcp:close(Sock), 47 | {State, #sample{node=node(), service=Service, values=Values}}; 48 | {error, Reason} -> 49 | log4erl:error("Error reading from ~p:~p: ~p", [Host, Port, Reason]), 50 | {State, #sample{node=node(), service=Service, values=[]}} 51 | end; 52 | {error, Reason} -> 53 | log4erl:error("Couldn't connect to ~p:~p: ~p", [Host, Port, Reason]), 54 | {State, #sample{node=node(), service=Service, values=[]}} 55 | end. 56 | 57 | %% @doc Stop collector. 58 | stop(_State, _Service) -> 59 | ok. 60 | 61 | %% =================================================================== 62 | %% Utility functions 63 | %% =================================================================== 64 | 65 | %% @doc Extract the socket parameters from service config. 66 | munin_node_params(Service) -> 67 | Config = Service#service.config, 68 | Host = proplists:get_value(host, Config), 69 | Port = proplists:get_value(port, Config, 4949), 70 | {ok, Opts} = application:get_env(munin_node_sock_opts), 71 | Plugin = proplists:get_value(plugin, Config), 72 | {Host, Port, Opts, Plugin}. 73 | 74 | %% @doc Fetch values from socket connection to Munin node. 75 | fetch(Sock, Plugin) -> 76 | ok = gen_tcp:send(Sock, rolf_util:string_format("fetch ~p\n", [Plugin])), 77 | read_values(Sock). 78 | 79 | %% @doc Read values from Sock until . on it's own on a line is encountered. 80 | read_values(Sock) -> 81 | read_values(Sock, []). 82 | read_values(Sock, Values) -> 83 | case gen_tcp:recv(Sock, 0) of 84 | {ok, Data} -> 85 | case Data of 86 | ".\n" -> 87 | log4erl:debug("Munin node fetch complete: ~p", [Values]), 88 | Values; 89 | Line -> 90 | log4erl:debug("Munin node fetch got: ~p", [Line]), 91 | read_values(Sock, [parse_line(Line)|Values]) 92 | end; 93 | {error, closed} -> 94 | Values 95 | end. 96 | 97 | %% @doc Parse a line returned by Munin node. E.g. "blah.value 99\n" 98 | parse_line(Line) -> 99 | K = list_to_atom(hd(string:tokens(Line, "."))), 100 | case rolf_util:list_to_num(hd(tl(string:tokens(Line, " ")))) of 101 | error -> {K, undefined}; 102 | S -> {K, S} 103 | end. 104 | 105 | %% =================================================================== 106 | %% Tests 107 | %% =================================================================== 108 | 109 | -ifdef(TEST). 110 | 111 | parse_line_test() -> 112 | ?assertEqual({blah, 99}, parse_line("blah.value 99\n")), 113 | ?assertEqual({blah, undefined}, parse_line("blah.value U\n")). 114 | 115 | -endif. 116 | -------------------------------------------------------------------------------- /apps/rolf/src/rolf.app.src: -------------------------------------------------------------------------------- 1 | %% vim: ft=erlang 2 | %% @doc Rolf app config. 3 | %% @author Ben Godfrey [http://aftnn.org/] 4 | %% @copyright 2011 Ben Godfrey 5 | %% @version 1.0.0 6 | %% 7 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 8 | %% Copyright (C) 2011 Ben Godfrey. 9 | %% 10 | %% This program is free software: you can redistribute it and/or modify 11 | %% it under the terms of the GNU General Public License as published by 12 | %% the Free Software Foundation, either version 3 of the License, or 13 | %% (at your option) any later version. 14 | %% 15 | %% This program is distributed in the hope that it will be useful, 16 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | %% GNU General Public License for more details. 19 | %% 20 | %% You should have received a copy of the GNU General Public License 21 | %% along with this program. If not, see . 22 | 23 | {application, rolf, [ 24 | {description, "System monitoring and graphing tool like Munin or collectd"}, 25 | {vsn, "0.1"}, 26 | {registered, []}, 27 | {applications, [kernel, stdlib, sasl, log4erl]}, 28 | {mod, {rolf_app, []}}, 29 | {env, [{recorders, []}, 30 | {services_config, "etc/services.config"}, 31 | {log4erl_config, "etc/log4erl.conf"}, 32 | {plugin_dir, "plugins"}, 33 | {plugin_default_freq, 10}, 34 | {plugin_default_timeout_multiple, 3}, 35 | {plugin_default_archives, [{1, 360}, % 1hr of 10s averages 36 | {30, 288}, % 1d of 5m averages 37 | {180, 336}, % 7d of 30m averages 38 | {8640, 365}]}, % 1y of 1d averages 39 | {plugin_default_type, gauge}, 40 | {plugin_default_draw, line}, 41 | {rrd_dir, "data"}, 42 | {rrd_ext, "rrd"}, 43 | {munin_node_sock_opts, [list, {packet, line}, {active, false}]}]}]}. 44 | -------------------------------------------------------------------------------- /apps/rolf/src/rolf.erl: -------------------------------------------------------------------------------- 1 | %% @doc Rolf startup. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf). 23 | 24 | %% API 25 | -export([start/0, stop/0]). 26 | 27 | start() -> 28 | application:start(sasl), 29 | application:start(log4erl), 30 | application:start(rolf). 31 | 32 | stop() -> 33 | application:stop(rolf), 34 | application:stop(log4erl), 35 | application:stop(sasl). 36 | -------------------------------------------------------------------------------- /apps/rolf/src/rolf_app.erl: -------------------------------------------------------------------------------- 1 | %% @doc Rolf app startup. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf_app). 23 | 24 | -behaviour(application). 25 | 26 | %% Application callbacks 27 | -export([start/0, start/2, stop/1]). 28 | 29 | %% =================================================================== 30 | %% Application callbacks 31 | %% =================================================================== 32 | 33 | start() -> 34 | start(normal, undefined). 35 | 36 | start(_StartType, _StartArgs) -> 37 | configure_logger(), 38 | log4erl:info("Starting"), 39 | CResult = rolf_collector_sup:start_link(), 40 | case rolf_recorder:is_recorder() of 41 | true -> 42 | log4erl:info("~p is a recorder", [node()]), 43 | rolf_recorder_sup:start_link(); 44 | _ -> 45 | log4erl:info("~p is a collector only", [node()]), 46 | announce_collector(), 47 | CResult 48 | end. 49 | 50 | stop(_State) -> 51 | ok. 52 | 53 | %% =================================================================== 54 | %% Utility functions 55 | %% =================================================================== 56 | 57 | %% @doc Load log4erl configuration. 58 | configure_logger() -> 59 | {ok, ConfigFilename} = application:get_env(log4erl_config), 60 | log4erl:conf(ConfigFilename). 61 | 62 | %% @doc Announce collector start to recorders. 63 | announce_collector() -> 64 | lists:foreach(fun net_adm:ping/1, rolf_recorder:recorders()). 65 | -------------------------------------------------------------------------------- /apps/rolf/src/rolf_collector.erl: -------------------------------------------------------------------------------- 1 | %% @doc Define a behaviour for collectors. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf_collector). 23 | 24 | %% Behaviour definition callbacks 25 | -export([behaviour_info/1]). 26 | 27 | %% @doc Return behaviour information. 28 | behaviour_info(callbacks) -> [{start, 1}, {collect, 2}, {stop, 2}]; 29 | behaviour_info(_) -> undefined. 30 | -------------------------------------------------------------------------------- /apps/rolf/src/rolf_collector_sup.erl: -------------------------------------------------------------------------------- 1 | %% @doc Rolf collector node supervisor. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf_collector_sup). 23 | 24 | -behaviour(supervisor). 25 | 26 | %% API 27 | -export([start_link/0, start_services/1]). 28 | 29 | %% supervisor callbacks 30 | -export([init/1]). 31 | 32 | -include("rolf.hrl"). 33 | 34 | %% =================================================================== 35 | %% API functions 36 | %% =================================================================== 37 | 38 | start_link() -> 39 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 40 | 41 | %% =================================================================== 42 | %% Supervisor callbacks 43 | %% =================================================================== 44 | 45 | %% @doc Collector supervisor initially does nothing. The recorder will send 46 | %% configuration. 47 | init([]) -> 48 | SupFlags = {simple_one_for_one, 1, 10}, 49 | ChildTemplate = {rolf_service, 50 | {rolf_service, start_link, []}, 51 | permanent, 2000, worker, [rolf_service]}, 52 | {ok, {SupFlags, [ChildTemplate]}}. 53 | 54 | %% @doc Start a set of service process. Called by 55 | %% rolf_recorder:start_collectors. 56 | start_services([]) -> ok; 57 | start_services([S|Services]) -> 58 | log4erl:info("Starting ~p service on ~p", [S#service.name, node()]), 59 | supervisor:start_child(?MODULE, [S]), 60 | rolf_service:start_emitting(S#service.name), 61 | start_services(Services). 62 | -------------------------------------------------------------------------------- /apps/rolf/src/rolf_config_validation.erl: -------------------------------------------------------------------------------- 1 | %% Look at lists:concat! 2 | 3 | %% Validate plugin values 4 | 5 | %% @doc Validate a list of values returned by a plugin. 6 | values_ok(Values, Metrics) when is_list(Values) -> 7 | lists:foldl(fun(V) -> value_ok(V, Metrics), Values); 8 | values_ok(_, _Metrics) -> false. 9 | 10 | %% @doc Validate a single value returned by a plugin. 11 | value_ok({Name, Value}, Metrics) when is_atom(Name) and is_int(Value) -> 12 | lists:member(Name, Metrics); 13 | value_ok(_, _Metrics) -> false. 14 | 15 | %% Validate (normalised) configuration 16 | 17 | %% - Node is atom() 18 | %% - Plugins all exist 19 | %% - Service names are unique 20 | %% - Options is [{atom(), term()}] 21 | -------------------------------------------------------------------------------- /apps/rolf/src/rolf_plugin.erl: -------------------------------------------------------------------------------- 1 | %% @doc Represent a service plugin, which gathers data. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf_plugin). 23 | 24 | %% API 25 | -export([list/0, load/2]). 26 | 27 | -include("rolf.hrl"). 28 | 29 | %% =================================================================== 30 | %% Configuration 31 | %% =================================================================== 32 | 33 | %% @doc List available plugins. 34 | list() -> 35 | {ok, PluginDir} = application:get_env(plugin_dir), 36 | list(PluginDir). 37 | 38 | %% @doc List available plugins in Dir. 39 | list(Dir) -> 40 | ConfigPat = filename:join([Dir, "*", "*.config"]), 41 | Configs = filelib:wildcard(ConfigPat), 42 | [configfilename_to_atom(C) || C <- Configs]. 43 | 44 | %% @doc Translate a config file pathname to an atom 45 | configfilename_to_atom(CFName) -> 46 | list_to_atom(filename:rootname(filename:basename(CFName))). 47 | 48 | %% @doc Load plugin config from file. 49 | load(Plugin, Opts) -> 50 | {ok, Config} = file:consult(config_path(Plugin)), 51 | parse(Plugin, propmerge(Config, Opts)). 52 | 53 | %% @doc Get path to a plugin's config file. 54 | config_path(Plugin) -> 55 | PluginStr = atom_to_list(Plugin), 56 | CfgName = string:join([PluginStr, "config"], "."), 57 | {ok, PluginDir} = application:get_env(plugin_dir), 58 | filename:join([PluginDir, PluginStr, CfgName]). 59 | 60 | %% @doc Parse config file contents into a service record. 61 | parse(Plugin, Config) -> 62 | {ok, PluginDefaultFreq} = application:get_env(plugin_default_freq), 63 | {ok, PluginDefaultTimeoutMultiple} = application:get_env(plugin_default_timeout_multiple), 64 | {ok, PluginDefaultArchives} = application:get_env(plugin_default_archives), 65 | Freq = proplists:get_value(frequency, Config, PluginDefaultFreq), 66 | #service{ 67 | plugin=Plugin, 68 | module=proplists:get_value(module, Config, rolf_command), 69 | command=parse_command(Plugin, Config), 70 | frequency=Freq, 71 | timeout=proplists:get_value(timeout, Config, Freq * PluginDefaultTimeoutMultiple), 72 | archives=proplists:get_value(archives, Config, PluginDefaultArchives), 73 | graph_title=proplists:get_value(graph_title, Config, atom_to_list(Plugin)), 74 | graph_vlabel=proplists:get_value(graph_vlabel, Config, ""), 75 | metrics=parse_metrics(Config), 76 | config=Config 77 | }. 78 | 79 | %% @doc Parse the command for this plugin. 80 | parse_command(Plugin, Config) -> 81 | case proplists:get_value(command, Config, undefined) of 82 | undefined -> 83 | undefined; 84 | Cmd -> 85 | external_path(Plugin, Cmd) 86 | end. 87 | 88 | %% @doc Return the full path to an external program. 89 | external_path(Plugin, Cmd) -> 90 | {ok, PluginDir} = application:get_env(plugin_dir), 91 | filename:join([PluginDir, atom_to_list(Plugin), Cmd]). 92 | 93 | %% @doc Parse config for a list of metrics into list of metric records. 94 | parse_metrics(Config) -> 95 | MetricCfg = proplists:get_value(metrics, Config, []), 96 | [parse_metric(M) || M <- MetricCfg]. 97 | 98 | %% @doc Parse config for a single metric into a metric record. 99 | parse_metric({Metric, MetricCfg}) -> 100 | {ok, PluginDefaultType} = application:get_env(plugin_default_type), 101 | {ok, PluginDefaultDraw} = application:get_env(plugin_default_draw), 102 | #metric{ 103 | name=Metric, 104 | label=proplists:get_value(label, MetricCfg, ""), 105 | type=proplists:get_value(type, MetricCfg, PluginDefaultType), 106 | draw=proplists:get_value(draw, MetricCfg, PluginDefaultDraw), 107 | min=proplists:get_value(min, MetricCfg, undefined), 108 | max=proplists:get_value(max, MetricCfg, undefined), 109 | colour=proplists:get_value(colour, MetricCfg, undefined) 110 | }. 111 | 112 | propmerge(L1, L2) -> 113 | dict:to_list(dict:merge(fun(_K, _V1, V2) -> V2 end, dict:from_list(L1), dict:from_list(L2))). 114 | 115 | %% =================================================================== 116 | %% Tests 117 | %% =================================================================== 118 | 119 | -ifdef(TEST). 120 | 121 | configfilename_to_atom_test() -> 122 | ?assertEqual(disk, configfilename_to_atom("plugins/disk/disk.config")). 123 | 124 | config_path_test() -> 125 | {ok, PluginDir} = application:get_env(plugin_dir), 126 | Path = filename:join([PluginDir, "loadtime", "loadtime.config"]), 127 | ?assertEqual(Path, config_path(loadtime)). 128 | 129 | parse_test() -> 130 | Input = [{command, "loadtime.sh"}, 131 | {frequency, 10}, 132 | {graph_title, "Load Time"}, 133 | {graph_vlabel, "Secs"}, 134 | {metrics, [{loadtime, [{label, "Load Time"}, 135 | {type, gauge}, 136 | {draw, areastack}, 137 | {min, 0}, 138 | {colour, "#0091FF"}]}]}], 139 | Output = parse(loadtime, Input), 140 | ?assertEqual(loadtime, Output#service.plugin), 141 | ?assertEqual(undefined, Output#service.name), 142 | ?assertEqual(10, Output#service.frequency), 143 | ?assertEqual("plugins/loadtime/loadtime.sh", Output#service.command), 144 | ?assertEqual(rolf_command, Output#service.module). 145 | 146 | parse_options_test() -> 147 | Input = [{command, "loadtime.sh"}, 148 | {unit, mb}], 149 | Output = parse(loadtime, Input), 150 | ?assertEqual(mb, proplists:get_value(unit, Output#service.config)). 151 | 152 | propmerge_test() -> 153 | ?assertEqual([{a, 1}, {b, 2}], propmerge([{a, 1}], [{b, 2}])), 154 | ?assertEqual([{a, 2}], propmerge([{a, 1}], [{a, 2}])). 155 | 156 | -endif. 157 | -------------------------------------------------------------------------------- /apps/rolf/src/rolf_recorder.erl: -------------------------------------------------------------------------------- 1 | %% @doc gen_server to which services can send samples for recording. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf_recorder). 23 | 24 | -behaviour(gen_server). 25 | 26 | %% API 27 | -export([config/0, recorders/0, live_recorders/0, is_recorder/0, start_link/0, 28 | stop/0, store/1]). 29 | 30 | %% gen_server callbacks 31 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, 32 | code_change/3]). 33 | 34 | -include("rolf.hrl"). 35 | 36 | %% =================================================================== 37 | %% API 38 | %% =================================================================== 39 | 40 | %% @doc Load configuration of recorders, collectors and services. 41 | config() -> 42 | {ok, ConfigFilename} = application:get_env(services_config), 43 | log4erl:debug("Loading config from ~p", [ConfigFilename]), 44 | case catch file:consult(ConfigFilename) of 45 | {ok, Config} -> 46 | log4erl:debug("Config: ~p", [Config]), 47 | Config; 48 | Else -> 49 | log4erl:error("Couldn't parse config file ~p: ~p", [ConfigFilename, Else]) 50 | end. 51 | 52 | %% @doc Return list of recorders. 53 | recorders() -> 54 | case application:get_env(recorders) of 55 | {ok, Recorders} -> Recorders; 56 | _ -> [] 57 | end. 58 | 59 | %% @doc Return list of live recorders. 60 | live_recorders() -> 61 | [R || R <- recorders(), net_adm:ping(R) =:= pong]. 62 | 63 | %% @doc Return true if current node is a recorder. 64 | is_recorder() -> 65 | lists:member(node(), recorders()). 66 | 67 | %% @doc Start a recorder on this node. 68 | start_link() -> 69 | gen_server:start_link({global, ?MODULE}, ?MODULE, [], []). 70 | 71 | %% @doc Stop recorder. 72 | stop() -> 73 | gen_server:call({global, ?MODULE}, stop). 74 | 75 | %% @doc Pass samples to all recorders in the cluster. 76 | store(Sample) -> 77 | gen_server:cast({global, ?MODULE}, {store, Sample}). 78 | 79 | %% =================================================================== 80 | %% gen_server callbacks 81 | %% =================================================================== 82 | 83 | init([]) -> 84 | process_flag(trap_exit, true), 85 | Config = rolf_recorder:config(), 86 | 87 | % start errd_server 88 | case errd_server:start_link() of 89 | {ok, RRD} -> 90 | log4erl:debug("Started errd_server"), 91 | Collectors = parse_collector_config(Config), 92 | start_collectors(RRD, Collectors), 93 | net_kernel:monitor_nodes(true), 94 | log4erl:info("Recorder started"), 95 | {ok, #recorder{collectors=Collectors, rrd=RRD}}; 96 | Else -> 97 | log4erl:error("Error starting errd_server: ~p", [Else]), 98 | {stop, Else} 99 | end. 100 | 101 | %% @doc Log unhandled calls. 102 | handle_call(Req, From, Service) -> 103 | log4erl:debug("Unhandled call from ~p: ~p", [From, Req]), 104 | {reply, Service}. 105 | 106 | handle_cast({store, Sample}, #recorder{rrd=RRD}=State) -> 107 | Service = Sample#sample.service, 108 | log4erl:debug("~p sample from ~p, values: ~p", [Service#service.name, Sample#sample.node, Sample#sample.values]), 109 | rolf_rrd:update(RRD, Sample), 110 | {noreply, State}; 111 | 112 | %% @doc Log unhandled casts. 113 | handle_cast(Req, Service) -> 114 | log4erl:debug("Unhandled cast: ~p", [Req]), 115 | {noreply, Service}. 116 | 117 | %% @doc Handle nodeup messages from monitoring nodes. Start services if the node 118 | %% is a collector and this is the highest priority live recorder (first in the 119 | %% list from app.config). 120 | handle_info({nodeup, Node}, #recorder{collectors=Collectors, rrd=RRD}=State) -> 121 | log4erl:info("Node ~p up", [Node]), 122 | Primary = hd(live_recorders()), 123 | case node() of 124 | Primary -> 125 | case lists:keyfind(Node, 1, Collectors) of 126 | {N, Ss} -> 127 | log4erl:info("Configured node ~p", [Node]), 128 | start_services(N, Ss, RRD) 129 | end; 130 | _ -> 131 | noop 132 | end, 133 | {noreply, State}; 134 | 135 | %% @doc Handle nodedown messages from monitoring nodes. 136 | handle_info({nodedown, Node}, State) -> 137 | log4erl:info("Node ~p down", [Node]), 138 | {noreply, State}; 139 | 140 | %% @doc Log unhandled info messages. 141 | handle_info(Info, Service) -> 142 | log4erl:debug("Unhandled info: ~p", [Info]), 143 | {noreply, Service}. 144 | 145 | terminate(_Reason, #recorder{rrd=RRD}) -> 146 | errd_server:stop(RRD), 147 | log4erl:info("Recorder stopped"). 148 | 149 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 150 | 151 | %% =================================================================== 152 | %% Utility functions 153 | %% =================================================================== 154 | 155 | %% @doc Parse node service definitions from services.config. Return list of 156 | %% {Node, Service} tuples. 157 | %% @spec parse_collector_config([term()]) -> [{node(), [{Plugin, Name, Opts}]}] 158 | parse_collector_config(Defs) -> 159 | parse_collector_config(Defs, []). 160 | 161 | %% @doc Parse node service defintions and populate accumulator. Don't use this 162 | %% function, call parse_collector_config/1. 163 | parse_collector_config([{node, Node, Services}|Defs], Acc) -> 164 | NormalisedServices = [normalise_service_config(S) || S <- Services], 165 | parse_collector_config(Defs, [{Node, NormalisedServices}|Acc]); 166 | parse_collector_config([_|Defs], Acc) -> 167 | parse_collector_config(Defs, Acc); 168 | parse_collector_config([], Acc) -> 169 | Acc. 170 | 171 | %% @doc Normalise various short-hand versions of service definition accepted in 172 | %% services.config. 173 | normalise_service_config(Plugin) when is_atom(Plugin) -> 174 | {Plugin, Plugin, []}; 175 | normalise_service_config({Plugin}) -> 176 | {Plugin, Plugin, []}; 177 | normalise_service_config({Plugin, Opts}) when is_list(Opts) -> 178 | {Plugin, Plugin, Opts}; 179 | normalise_service_config({Plugin, Name}) when is_atom(Name) -> 180 | {Plugin, Name, []}; 181 | normalise_service_config({Plugin, Name, Opts}) when is_atom(Name) and is_list(Opts) -> 182 | {Plugin, Name, Opts}. 183 | 184 | %% @doc Ping collector nodes and give them service configuration. 185 | start_collectors(RRD, Collectors) -> 186 | LiveCollectors = connect_cluster(Collectors), 187 | log4erl:info("Starting collectors"), 188 | lists:foreach(fun({N, Ss}) -> start_services(RRD, N, Ss) end, LiveCollectors). 189 | 190 | %% @doc Ping nodes that we're expected to record from. 191 | connect_cluster(Config) -> 192 | [{N, S} || {N, S} <- Config, net_adm:ping(N) =:= pong]. 193 | 194 | %% @doc Start collectors on a set of nodes. 195 | start_services(RRD, Node, SDefs) -> 196 | Recs = live_recorders(), 197 | Services = [load_service(Plugin, Name, Opts, Recs) || {Plugin, Name, Opts} <- SDefs], 198 | lists:foreach(fun(S) -> rolf_rrd:ensure(RRD, Node, S) end, Services), 199 | rpc:call(Node, rolf_collector_sup, start_services, [Services]). 200 | 201 | load_service(Plugin, Name, Opts, Recs) -> 202 | S = rolf_plugin:load(Plugin, Opts), 203 | S#service{name=Name, recorders=Recs}. 204 | -------------------------------------------------------------------------------- /apps/rolf/src/rolf_recorder_sup.erl: -------------------------------------------------------------------------------- 1 | %% @doc Rolf supervisor configuration. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf_recorder_sup). 23 | 24 | -behaviour(supervisor). 25 | 26 | %% API 27 | -export([start_link/0]). 28 | 29 | %% supervisor callbacks 30 | -export([init/1]). 31 | 32 | -include("rolf.hrl"). 33 | 34 | %% =================================================================== 35 | %% API functions 36 | %% =================================================================== 37 | 38 | start_link() -> 39 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 40 | 41 | %% =================================================================== 42 | %% Supervisor callbacks 43 | %% =================================================================== 44 | 45 | %% @doc Supervisor configuration for rolf recorder. Start rolf_recorder 46 | %% gen_server which will start an errd_server. 47 | init([]) -> 48 | Recorder = {rolf_recorder, {rolf_recorder, start_link, []}, 49 | permanent, 2000, worker, [rolf_recorder]}, 50 | {ok, {{one_for_one, 1, 10}, [Recorder]}}. 51 | -------------------------------------------------------------------------------- /apps/rolf/src/rolf_rrd.erl: -------------------------------------------------------------------------------- 1 | %% @doc Utilities for managing RRD files with errd. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf_rrd). 23 | 24 | %% API 25 | -export([ensure/3, update/2]). 26 | 27 | -include_lib("errd/include/errd.hrl"). 28 | -include("rolf.hrl"). 29 | 30 | %% =================================================================== 31 | %% API 32 | %% =================================================================== 33 | 34 | %% @doc Ensure data dir and RRD file for Service on Node exist. 35 | ensure(RRD, Node, Service) -> 36 | Path = rrd_path(Node, Service), 37 | case filelib:ensure_dir(Path) of 38 | {error, Reason} -> 39 | log4erl:error("Couldn't create RRD dir ~p: ~p", [Path, Reason]), 40 | {error, Reason}; 41 | ok -> 42 | case filelib:is_file(Path) of 43 | false -> create(RRD, Path, Service); 44 | true -> ok 45 | end 46 | end. 47 | 48 | %% @doc Create an RRD file for a service. Metrics should be a list of 49 | %% {Name, Type} tuples, e.g. [{signups, counter}, {downloads, counter}]. Type 50 | %% must be one of gauge, counter, derive or absolute. 51 | create(RRD, Path, Service) -> 52 | log4erl:info("Creating ~p", [Path]), 53 | send_command(RRD, make_rrd_create(Path, Service)). 54 | 55 | %% @doc Update an RRD file with a new sample. 56 | update(RRD, #sample{node=Node, service=Service, values=Values}) -> 57 | Path = rrd_path(Node, Service), 58 | send_command(RRD, make_update(Path, Values)). 59 | 60 | %% =================================================================== 61 | %% Utility functions 62 | %% =================================================================== 63 | 64 | %% @doc Send command to RRD server, return ok or {error, Reason}. 65 | send_command(RRD, Cmd) -> 66 | log4erl:debug("RRD command: ~p", [Cmd]), 67 | FormattedCmd = errd_command:format(Cmd), 68 | case errd_server:raw(RRD, FormattedCmd) of 69 | {error, Reason} -> 70 | log4erl:error("errd_server error: ~p", [Reason]), 71 | {error, Reason}; 72 | {ok, _Lines} -> 73 | ok 74 | end. 75 | 76 | %% @doc Create absolute path for RRD file for Service running on Node. A single 77 | %% RRD file contains values for multiple metrics (data sources). 78 | rrd_path(Node, Service) -> 79 | {ok, RRDExt} = application:get_env(rrd_ext), 80 | Filename = string:join([atom_to_list(Service#service.name), RRDExt], "."), 81 | {ok, RRDDir} = application:get_env(rrd_dir), 82 | filename:join([RRDDir, atom_to_list(Node), Filename]). 83 | 84 | %% @doc Generate command to create an RRD with a set of metrics. 85 | make_rrd_create(Path, #service{frequency=Frequency, timeout=Timeout, 86 | archives=Archives, metrics=Metrics}) -> 87 | RRAs = [make_rra(S, C) || {S, C} <- Archives], 88 | DSs = [make_ds(M, Timeout) || M <- Metrics], 89 | #rrd_create{file=Path, step=Frequency, ds_defs=DSs, rra_defs=RRAs}. 90 | 91 | %% @doc Create an average rrd_rra record from a step and a count. 92 | make_rra(Step, Count) -> 93 | #rrd_rra{cf=average, args=rolf_util:string_format("0.5:~b:~b", [Step, Count])}. 94 | 95 | %% @doc Make a rrd_ds record from a metric definition. 96 | make_ds(#metric{name=Name, type=Type}, Timeout) -> 97 | #rrd_ds{name=atom_to_list(Name), type=Type, args=rolf_util:string_format("~b:U:U", [Timeout])}. 98 | 99 | %% @doc Make an rrd_update record from an RRD path and a set of values. 100 | make_update(Path, Values) -> 101 | Updates = [make_ds_update(M, V) || {M, V} <- Values], 102 | #rrd_update{file=Path, updates=Updates}. 103 | 104 | %% @doc Make an rrd_ds_update record for a measurement. 105 | make_ds_update(Metric, Value) -> 106 | #rrd_ds_update{name=atom_to_list(Metric), value=mochinum:digits(Value)}. 107 | 108 | %% =================================================================== 109 | %% Tests 110 | %% =================================================================== 111 | 112 | -ifdef(TEST). 113 | 114 | rrd_path_test() -> 115 | {ok, RRDDir} = application:get_env(rrd_dir), 116 | Path = rrd_path(frank@josie, #service{name=loadtime}), 117 | ?assertEqual(filename:join([RRDDir, "frank@josie", "loadtime.rrd"]), Path). 118 | 119 | make_rrd_create_test() -> 120 | {ok, RRDDir} = application:get_env(rrd_dir), 121 | Path = filename:join([RRDDir, "frank@josie", "loadtime.rrd"]), 122 | DSs = [#rrd_ds{name="loadtime", type=gauge, args="900:U:U"}], 123 | RRAs = [#rrd_rra{cf=average, args="0.5:1:60"}], 124 | Metrics = [#metric{name=loadtime, type=gauge}], 125 | Create = make_rrd_create(Path, #service{frequency=60, timeout=900, 126 | archives=[{1, 60}], metrics=Metrics}), 127 | ?assertEqual(#rrd_create{file=Path, step=60, ds_defs=DSs, rra_defs=RRAs}, Create). 128 | 129 | make_update_test() -> 130 | {ok, RRDDir} = application:get_env(rrd_dir), 131 | Path = filename:join([RRDDir, "frank@josie", "loadtime.rrd"]), 132 | Update = make_update(Path, [{loadtime, 0.99}]), 133 | DSUpdates = [#rrd_ds_update{name="loadtime", value=0.99}], 134 | Expected = #rrd_update{file=Path, updates=DSUpdates}, 135 | ?assertEqual(Expected, Update). 136 | 137 | -endif. 138 | -------------------------------------------------------------------------------- /apps/rolf/src/rolf_service.erl: -------------------------------------------------------------------------------- 1 | %% @doc gen_server which provides monitoring information for a single service on 2 | %% a machine. 3 | %% @author Ben Godfrey [http://aftnn.org/] 4 | %% @copyright 2011 Ben Godfrey 5 | %% @version 1.0.0 6 | %% 7 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 8 | %% Copyright (C) 2011 Ben Godfrey. 9 | %% 10 | %% This program is free software: you can redistribute it and/or modify 11 | %% it under the terms of the GNU General Public License as published by 12 | %% the Free Software Foundation, either version 3 of the License, or 13 | %% (at your option) any later version. 14 | %% 15 | %% This program is distributed in the hope that it will be useful, 16 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | %% GNU General Public License for more details. 19 | %% 20 | %% You should have received a copy of the GNU General Public License 21 | %% along with this program. If not, see . 22 | 23 | -module(rolf_service). 24 | 25 | -behaviour(gen_server). 26 | 27 | %% API 28 | -export([start_link/1, stop/1, publish/1, start_emitting/1, stop_emitting/1]). 29 | 30 | %% gen_server callbacks 31 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, 32 | code_change/3]). 33 | 34 | -include("rolf.hrl"). 35 | 36 | %% =================================================================== 37 | %% API 38 | %% =================================================================== 39 | 40 | %% @doc Start a service using Service as initial state. 41 | start_link(Service) -> 42 | gen_server:start_link({local, server_name(Service)}, ?MODULE, [Service], []). 43 | 44 | %% @doc Stop service Name. 45 | stop(Name) -> 46 | gen_server:call(server_name(Name), stop). 47 | 48 | %% @doc Start emitting samples. Emit one straight away and then set a timer to 49 | %% emit regularly. 50 | start_emitting(Name) -> 51 | gen_server:cast(server_name(Name), start_emitting). 52 | 53 | %% @doc Stop emitting samples. 54 | stop_emitting(Name) -> 55 | gen_server:cast(server_name(Name), stop_emitting). 56 | 57 | %% @doc Trigger polling of this service manually, useful for inspecting and debugging 58 | publish(Name) -> 59 | gen_server:cast(server_name(Name), publish). 60 | 61 | %% =================================================================== 62 | %% gen_server callbacks 63 | %% =================================================================== 64 | 65 | %% @doc Start service, create a timer which will sample for results regularly and 66 | %% publish them to the recorder. 67 | init([Service]) -> 68 | process_flag(trap_exit, true), 69 | net_kernel:monitor_nodes(true), 70 | CState = apply(Service#service.module, start, [Service]), 71 | {ok, {Service, CState}}. 72 | 73 | %% @doc Log unhandled calls. 74 | handle_call(Req, From, State) -> 75 | log4erl:debug("Unhandled call from ~p: ~p", [From, Req]), 76 | {reply, State}. 77 | 78 | handle_cast(start_emitting, {Service, CState}) -> 79 | Name = Service#service.name, 80 | Freq = Service#service.frequency, 81 | log4erl:info("~p started emitting (frequency ~p)", [Name, Freq]), 82 | apply(?MODULE, publish, [Name]), 83 | case timer:apply_interval(timer:seconds(Freq), ?MODULE, publish, [Name]) of 84 | {ok, TRef} -> 85 | {noreply, {Service#service{tref=TRef}, CState}}; 86 | _ -> 87 | {noreply, {Service, CState}} 88 | end; 89 | 90 | handle_cast(stop_emitting, {Service, CState}) -> 91 | log4erl:info("~p stopped emitting", [Service#service.name]), 92 | timer:cancel(Service#service.tref), 93 | {noreply, {#service{tref=undefined}, CState}}; 94 | 95 | handle_cast(publish, {Service, CState}) -> 96 | {CState1, Sample} = apply(Service#service.module, collect, [Service, CState]), 97 | send(Service#service.recorders, Sample), 98 | {noreply, {Service, CState1}}; 99 | 100 | %% @doc Log unhandled casts. 101 | handle_cast(Req, State) -> 102 | log4erl:debug("Unhandled cast: ~p", [Req]), 103 | {noreply, State}. 104 | 105 | %% @doc Handle nodeup messages from monitoring nodes. 106 | handle_info({nodeup, Node}, {Service, CState}) -> 107 | case lists:member(Node, rolf_recorder:recorders()) of 108 | true -> 109 | log4erl:info("Recorder ~p up", [Node]), 110 | OldRecs = Service#service.recorders, 111 | {noreply, {Service#service{recorders=[Node|OldRecs]}, CState}}; 112 | _ -> 113 | {noreply, {Service, CState}} 114 | end; 115 | 116 | %% @doc Handle nodedown messages from monitoring nodes. 117 | handle_info({nodedown, Node}, {Service, CState}) -> 118 | Live = rolf_recorder:live_recorders(), 119 | case Live of 120 | [] -> 121 | log4erl:info("Recorder ~p down, exiting", [Node]), 122 | {stop, no_recorders, {Service, CState}}; 123 | _ -> 124 | log4erl:info("Recorder ~p down", [Node]), 125 | {noreply, Service#service{recorders=Live}} 126 | end; 127 | 128 | %% @doc Log unhandled info messages. 129 | handle_info(Info, State) -> 130 | log4erl:debug("Unhandled info: ~p", [Info]), 131 | {noreply, State}. 132 | 133 | %% @doc Terminate 134 | terminate(_Reason, {Service, CState}) -> 135 | stop_emitting(Service), 136 | Module = Service#service.module, 137 | apply(Module, stop, [Service, CState]), 138 | ok. 139 | 140 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 141 | 142 | %% =================================================================== 143 | %% Utility functions 144 | %% =================================================================== 145 | 146 | %% @doc Get canonical name of service from name atom or service record. 147 | server_name(Name) when is_atom(Name) -> 148 | list_to_atom(string:join([atom_to_list(A) || A <- [?MODULE, Name]], "_")); 149 | server_name(#service{name=Name}) -> 150 | server_name(Name). 151 | 152 | %% @doc Send sample to all live recorders. 153 | send(Recorders, Sample) -> 154 | lists:foreach(fun(R) -> rpc:call(R, rolf_recorder, store, [Sample]) end, 155 | Recorders). 156 | 157 | %% =================================================================== 158 | %% Tests 159 | %% =================================================================== 160 | 161 | -ifdef(TEST). 162 | 163 | server_name_test() -> 164 | Name = list_to_atom("rolf_service_loadtime"), 165 | ?assertEqual(Name, server_name(loadtime)), 166 | ?assertEqual(Name, server_name(#service{name=loadtime})). 167 | 168 | -endif. 169 | -------------------------------------------------------------------------------- /apps/rolf/src/rolf_util.erl: -------------------------------------------------------------------------------- 1 | %% @doc Plugin module for retrieving measurements from a munin node. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf_util). 23 | 24 | %% rolf_collector callbacks 25 | -export([string_format/2, list_to_num/1, strip_whitespace/1]). 26 | 27 | -include("rolf.hrl"). 28 | 29 | %% =================================================================== 30 | %% Utility functions 31 | %% =================================================================== 32 | 33 | %% @doc Sane string formatting. 34 | string_format(Pattern, Values) -> 35 | lists:flatten(io_lib:format(Pattern, Values)). 36 | 37 | %% @doc Coerce a string to a float or an integer. 38 | list_to_num(S) -> 39 | S1 = strip_whitespace(S), 40 | try list_to_float(S1) catch 41 | error:badarg -> 42 | try list_to_integer(S1) catch 43 | error:badarg -> error 44 | end 45 | end. 46 | 47 | %% @doc Strip surrounding whitespace from a string. 48 | strip_whitespace(S) -> 49 | strip_whitespace(S, []). 50 | strip_whitespace([C|Cs], S) when C == $\s; C == $\n; C == $\r; C == $\t -> 51 | S ++ strip_whitespace(Cs); 52 | strip_whitespace([C|Cs], S) -> 53 | S ++ [C|strip_whitespace(Cs)]; 54 | strip_whitespace([], S) -> 55 | S. 56 | 57 | %% =================================================================== 58 | %% Tests 59 | %% =================================================================== 60 | 61 | -ifdef(TEST). 62 | 63 | string_format_test() -> 64 | ?assertEqual("X:1:9.5:z", string_format("~s:~b:~.1f:~p", ["X", 1, 9.5, z])). 65 | 66 | list_to_num_test() -> 67 | ?assertEqual(99, list_to_num("99")), 68 | ?assertEqual(-1, list_to_num("-1")), 69 | ?assertEqual(0.999, list_to_num("0.999")), 70 | ?assertEqual(-3.14, list_to_num("-3.14")), 71 | ?assertEqual(99, list_to_num(" 99\n ")), 72 | ?assertEqual(error, list_to_num("monkey")). 73 | 74 | strip_whitespace_test() -> 75 | ?assertEqual("blah", strip_whitespace(" blah\n\t ")). 76 | 77 | -endif. 78 | -------------------------------------------------------------------------------- /apps/rolf/test/rolf_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% @doc Common test suite for Rolf. 2 | %% @author Ben Godfrey [http://aftnn.org/] 3 | %% @copyright 2011 Ben Godfrey 4 | %% @version 1.0.0 5 | %% 6 | %% Rolf - a monitoring and graphing tool like Munin or collectd. 7 | %% Copyright (C) 2011 Ben Godfrey. 8 | %% 9 | %% This program is free software: you can redistribute it and/or modify 10 | %% it under the terms of the GNU General Public License as published by 11 | %% the Free Software Foundation, either version 3 of the License, or 12 | %% (at your option) any later version. 13 | %% 14 | %% This program is distributed in the hope that it will be useful, 15 | %% but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | %% GNU General Public License for more details. 18 | %% 19 | %% You should have received a copy of the GNU General Public License 20 | %% along with this program. If not, see . 21 | 22 | -module(rolf_SUITE). 23 | -compile(export_all). 24 | 25 | -include_lib("common_test/include/ct.hrl"). 26 | 27 | %% =================================================================== 28 | %% Common Test callbacks 29 | %% =================================================================== 30 | 31 | %% Specify a list of all unit test functions 32 | all() -> [test1]. 33 | 34 | %% required, but can just return Config. this is a suite level setup function. 35 | init_per_suite(Config) -> 36 | %% do custom per suite setup here 37 | Config. 38 | 39 | %% required, but can just return Config. this is a suite level tear down function. 40 | end_per_suite(Config) -> 41 | %% do custom per suite cleanup here 42 | Config. 43 | 44 | %% optional, can do function level setup for all functions, 45 | %% or for individual functions by matching on TestCase. 46 | init_per_testcase(_TestCase, Config) -> 47 | %% do custom test case setup here 48 | Config. 49 | 50 | %% optional, can do function level tear down for all functions, 51 | %% or for individual functions by matching on TestCase. 52 | end_per_testcase(_TestCase, Config) -> 53 | %% do custom test case cleanup here 54 | Config. 55 | 56 | %% =================================================================== 57 | %% Test cases 58 | %% =================================================================== 59 | 60 | test1(_Config) -> 61 | %% write standard erlang code to test whatever you want 62 | %% use pattern matching to specify expected return values 63 | ok. 64 | 65 | -------------------------------------------------------------------------------- /bin/graph.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | now=`date +%s` 3 | start=`expr $now - 3600` 4 | rrdtool graph /var/www/aftnn/stuff/rolf_loadtime.png -s $start -S 10 \ 5 | -w 1359 -h 851 -l 0 -u 1000 -r \ 6 | -c BACK#191919 -c CANVAS#191919 -c SHADEA#191919 -c SHADEB#191919 -c FONT#ffffff \ 7 | 'DEF:bbc=data/rolf@127.0.0.1/loadtime.rrd:bbc:AVERAGE' \ 8 | 'DEF:guardian=data/rolf@127.0.0.1/loadtime.rrd:guardian:AVERAGE' \ 9 | 'DEF:lastminute=data/rolf@127.0.0.1/loadtime.rrd:lastminute:AVERAGE' \ 10 | 'DEF:aws=data/rolf@127.0.0.1/loadtime.rrd:aws:AVERAGE' \ 11 | 'DEF:appengine=data/rolf@127.0.0.1/loadtime.rrd:appengine:AVERAGE' \ 12 | 'DEF:twitter=data/rolf@127.0.0.1/loadtime.rrd:twitter:AVERAGE' \ 13 | 'DEF:argos=data/rolf@127.0.0.1/loadtime.rrd:argos:AVERAGE' \ 14 | 'DEF:aarouteplanner=data/rolf@127.0.0.1/loadtime.rrd:aarouteplanner:AVERAGE' \ 15 | 'DEF:ocado=data/rolf@127.0.0.1/loadtime.rrd:ocado:AVERAGE' \ 16 | 'DEF:dailymail=data/rolf@127.0.0.1/loadtime.rrd:dailymail:AVERAGE' \ 17 | 'LINE:bbc#0091ff' \ 18 | 'LINE:guardian#91ff00' \ 19 | 'LINE:lastminute#9100ff' \ 20 | 'LINE:aws#91ff00' \ 21 | 'LINE:appengine#ff0091' \ 22 | 'LINE:twitter#ff9100' \ 23 | 'LINE:argos#00ff91' \ 24 | 'LINE:aarouteplanner#cc0066' \ 25 | 'LINE:ocado#0066cc' \ 26 | 'LINE:dailymail#66cc00' 27 | -------------------------------------------------------------------------------- /doc/windows.md: -------------------------------------------------------------------------------- 1 | Rolf on Windows 2 | =============== 3 | 4 | 1. Install Erlang R14B02. 5 | 6 | http://www.erlang.org/download.html 7 | 8 | 2. Download Rolf release package (or generate your own with rebar). 9 | 10 | https://github.com/downloads/afternoon/rolf/rolf-0.1.zip 11 | 12 | 3. Add Rolf to Windows Services 13 | 14 | c:\erl5.8.3\erts-5.8.3\bin\erlsrv.exe add Rolf -c "Collects system data for monitoring." -w c:\rolf -m c:\erl5.8.3\erts-5.8.3\bin\start_erl.exe -debugtype reuse -args "-setcookie rolf123 -boot c:\rolf\release\0.1\rolf.boot -embedded -config c:\rolf\etc\app.config -args_file c:\rolf\etc\vm.args ++ -reldir c:\rolf\releases" 15 | c:\erl5.8.3\erts-5.8.3\bin\erlsrv.exe add Rolf -c "Collects system data for monitoring." -w c:\rolf -m c:\erl5.8.3\erts-5.8.3\bin\start_erl.exe -debugtype reuse -args "-setcookie rolf123 ++ -reldir c:\rolf\releases" 16 | 17 | 4. Start the service! 18 | 19 | c:\erl5.8.3\erts-5.8.3\bin\erlsrv.exe start Rolf 20 | -------------------------------------------------------------------------------- /plugins: -------------------------------------------------------------------------------- 1 | rel/files/plugins -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afternoon/rolf/7a5445d28363e4219be2947c2aaff58d72374cb8/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% vim: ft=erlang 2 | 3 | {sub_dirs, ["apps/rolf", "rel"]}. 4 | 5 | %% dependencies 6 | {deps, [{errd, ".*", {git, "git://github.com/Vagabond/errd.git", "master"}}, 7 | {log4erl, ".*", {git, "git://github.com/ahmednawras/log4erl.git", "master"}}, 8 | {plists, ".*", {git, "git://github.com/yrashk/plists.git", "master"}}]}. 9 | 10 | %% testing options 11 | {cover_enabled, true}. 12 | {erl_opts, [debug_info]}. 13 | {eunit_opts, [verbose, {report, {eunit_surefire, [{dir, "."}]}}]}. 14 | -------------------------------------------------------------------------------- /rel/files/app.config: -------------------------------------------------------------------------------- 1 | %% vim: ft=erlang 2 | [{rolf, [{recorders, ['rolf@127.0.0.1']}]}, 3 | {sasl, [{sasl_error_logger, {file, "log/sasl-error.log"}}, 4 | {errlog_type, error}, 5 | {error_logger_mf_dir, "log/sasl"}, 6 | {error_logger_mf_maxbytes, 10485760}, 7 | {error_logger_mf_maxfiles, 5}]}]. 8 | -------------------------------------------------------------------------------- /rel/files/erl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## This script replaces the default "erl" in erts-VSN/bin. This is necessary 4 | ## as escript depends on erl and in turn, erl depends on having access to a 5 | ## bootscript (start.boot). Note that this script is ONLY invoked as a side-effect 6 | ## of running escript -- the embedded node bypasses erl and uses erlexec directly 7 | ## (as it should). 8 | ## 9 | ## Note that this script makes the assumption that there is a start_clean.boot 10 | ## file available in $ROOTDIR/release/VSN. 11 | 12 | # Determine the abspath of where this script is executing from. 13 | ERTS_BIN_DIR=$(cd ${0%/*} && pwd) 14 | 15 | # Now determine the root directory -- this script runs from erts-VSN/bin, 16 | # so we simply need to strip off two dirs from the end of the ERTS_BIN_DIR 17 | # path. 18 | ROOTDIR=${ERTS_BIN_DIR%/*/*} 19 | 20 | # Parse out release and erts info 21 | START_ERL=`cat $ROOTDIR/releases/start_erl.data` 22 | ERTS_VSN=${START_ERL% *} 23 | APP_VSN=${START_ERL#* } 24 | 25 | BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin 26 | EMU=beam 27 | PROGNAME=`echo $0 | sed 's/.*\\///'` 28 | CMD="$BINDIR/erlexec" 29 | export EMU 30 | export ROOTDIR 31 | export BINDIR 32 | export PROGNAME 33 | 34 | exec $CMD -boot $ROOTDIR/releases/$APP_VSN/start_clean ${1+"$@"} -------------------------------------------------------------------------------- /rel/files/log4erl.conf: -------------------------------------------------------------------------------- 1 | logger { 2 | file_appender file{ 3 | dir = "log", 4 | level = debug, 5 | file = "rolf", 6 | type = size, 7 | max = 100000, 8 | suffix = log, 9 | rotation = 5, 10 | format = '%I %L %l%n' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /rel/files/nodetool: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | %% ------------------------------------------------------------------- 3 | %% 4 | %% nodetool: Helper Script for interacting with live nodes 5 | %% 6 | %% ------------------------------------------------------------------- 7 | 8 | main(Args) -> 9 | %% Extract the args 10 | {RestArgs, TargetNode} = process_args(Args, [], undefined), 11 | 12 | %% See if the node is currently running -- if it's not, we'll bail 13 | case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of 14 | {true, pong} -> 15 | ok; 16 | {_, pang} -> 17 | io:format("Node ~p not responding to pings.\n", [TargetNode]), 18 | halt(1) 19 | end, 20 | 21 | case RestArgs of 22 | ["ping"] -> 23 | %% If we got this far, the node already responsed to a ping, so just dump 24 | %% a "pong" 25 | io:format("pong\n"); 26 | ["stop"] -> 27 | io:format("~p\n", [rpc:call(TargetNode, init, stop, [], 60000)]); 28 | ["restart"] -> 29 | io:format("~p\n", [rpc:call(TargetNode, init, restart, [], 60000)]); 30 | ["reboot"] -> 31 | io:format("~p\n", [rpc:call(TargetNode, init, reboot, [], 60000)]); 32 | ["rpc", Module, Function | RpcArgs] -> 33 | case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), [RpcArgs], 60000) of 34 | ok -> 35 | ok; 36 | {badrpc, Reason} -> 37 | io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), 38 | halt(1); 39 | _ -> 40 | halt(1) 41 | end; 42 | Other -> 43 | io:format("Other: ~p\n", [Other]), 44 | io:format("Usage: nodetool {ping|stop|restart|reboot}\n") 45 | end, 46 | net_kernel:stop(). 47 | 48 | process_args([], Acc, TargetNode) -> 49 | {lists:reverse(Acc), TargetNode}; 50 | process_args(["-setcookie", Cookie | Rest], Acc, TargetNode) -> 51 | erlang:set_cookie(node(), list_to_atom(Cookie)), 52 | process_args(Rest, Acc, TargetNode); 53 | process_args(["-name", TargetName | Rest], Acc, _) -> 54 | ThisNode = append_node_suffix(TargetName, "_maint_"), 55 | {ok, _} = net_kernel:start([ThisNode, longnames]), 56 | process_args(Rest, Acc, nodename(TargetName)); 57 | process_args(["-sname", TargetName | Rest], Acc, _) -> 58 | ThisNode = append_node_suffix(TargetName, "_maint_"), 59 | {ok, _} = net_kernel:start([ThisNode, shortnames]), 60 | process_args(Rest, Acc, nodename(TargetName)); 61 | process_args([Arg | Rest], Acc, Opts) -> 62 | process_args(Rest, [Arg | Acc], Opts). 63 | 64 | 65 | nodename(Name) -> 66 | case string:tokens(Name, "@") of 67 | [_Node, _Host] -> 68 | list_to_atom(Name); 69 | [Node] -> 70 | [_, Host] = string:tokens(atom_to_list(node()), "@"), 71 | list_to_atom(lists:concat([Node, "@", Host])) 72 | end. 73 | 74 | append_node_suffix(Name, Suffix) -> 75 | case string:tokens(Name, "@") of 76 | [Node, Host] -> 77 | list_to_atom(lists:concat([Node, Suffix, os:getpid(), "@", Host])); 78 | [Node] -> 79 | list_to_atom(lists:concat([Node, Suffix, os:getpid()])) 80 | end. 81 | -------------------------------------------------------------------------------- /rel/files/plugins/disk/disk.config: -------------------------------------------------------------------------------- 1 | %% vim: ft=erlang 2 | %% use this external program to collect data 3 | {command, "disk.sh"}. 4 | 5 | %% update frequency in seconds 6 | {frequency, 300}. 7 | 8 | %% number of seconds allowed before service becomes "unknown" 9 | {timeout, 3600}. 10 | 11 | %% define the RRAs (round robin archives) to be stored for this service 12 | {archives, [{1, 288}, % 1d of 5m averages 13 | {12, 336}, % 7d of 1hr averages 14 | {288, 365}]}. % 1y of 1d averages 15 | 16 | %% graph parameters 17 | {graph_title, "Disk Space"}. 18 | {graph_vlabel, "GB"}. 19 | 20 | %% metric configuration 21 | {metrics, [{freespace, [{label, "Free Space"}, 22 | {type, gauge}, 23 | {draw, areastack}, 24 | {min, 0}, 25 | {colour, "#66BDFF"}]}]}. 26 | -------------------------------------------------------------------------------- /rel/files/plugins/disk/disk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Get free space on root filesystem 3 | echo -n "freespace "; df -h / | tail -n 1 | awk '{ print $5 }' | cut -d % -f 1 4 | echo "." 5 | -------------------------------------------------------------------------------- /rel/files/plugins/loadtime/loadtime.config: -------------------------------------------------------------------------------- 1 | %% vim: ft=erlang 2 | %% use this Erlang module to collect data 3 | {module, rolf_loadtime}. 4 | 5 | %% update frequency in seconds 6 | {frequency, 5}. 7 | 8 | %% number of seconds allowed before service becomes "unknown" 9 | {timeout, 60}. 10 | 11 | %% define the RRAs (round robin archives) to be stored for this service 12 | {archives, [{1, 720}, % 1hr of 5s averages 13 | {6, 2880}, % 1d of 30s averages 14 | {360, 336}, % 7d of 30m averages 15 | {17280, 365}]}. % 1y of 1d averages 16 | 17 | %% graph parameters 18 | {graph_title, "Load Time"}. 19 | {graph_vlabel, "Secs"}. 20 | 21 | %% metric configuration - tricky for this plugin as each url is a metric 22 | {metrics, [{loadtime, [{label, "Load Time"}, 23 | {type, gauge}, 24 | {draw, areastack}, 25 | {min, 0}, 26 | {colour, "#0091FF"}]}]}. 27 | 28 | %% url to measure 29 | {url, "http://localhost/"}. 30 | -------------------------------------------------------------------------------- /rel/files/plugins/loadtime/loadtime.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Get load time of a bunch of sites (sequentially) 3 | for url in $*; do 4 | curl -s -o /dev/null -w "loadtime %{time_total}\n" "$url" 5 | done 6 | echo "." 7 | -------------------------------------------------------------------------------- /rel/files/plugins/uptime/uptime.config: -------------------------------------------------------------------------------- 1 | %% vim: ft=erlang 2 | %% use this Erlang module to collect data 3 | {module, rolf_munin_node}. 4 | 5 | %% update frequency in seconds 6 | {frequency, 300}. 7 | 8 | %% number of seconds allowed before service becomes "unknown" 9 | {timeout, 600}. 10 | 11 | %% define the RRAs (round robin archives) to be stored for this service 12 | %% matches Munin (http://munin-monitoring.org/browser/trunk/master/lib/Munin/Master/UpdateWorker.pm) 13 | {archives, [{1, 576}, % 2d of 5m averages 14 | {6, 432}, % 9d of 30m averages 15 | {24, 540}, % 45d of 2h averages 16 | {288, 450}]}. % 450d of 1d averages 17 | 18 | %% graph parameters 19 | {graph_title, "Uptime"}. 20 | {graph_vlabel, "Days"}. 21 | 22 | %% metric configuration - tricky for this plugin as each url is a metric 23 | {metrics, [{uptime, [{label, "Uptime"}, 24 | {type, gauge}, 25 | {min, 0}, 26 | {draw, areastack}, 27 | {colour, "#0091FF"}]}]}. 28 | 29 | %% host to collect from 30 | {host, localhost}. 31 | 32 | %% Munin plugin to fetch 33 | {plugin, uptime}. 34 | -------------------------------------------------------------------------------- /rel/files/rolf: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- tab-width:4;indent-tabs-mode:nil -*- 3 | # ex: ts=4 sw=4 et 4 | 5 | RUNNER_SCRIPT_DIR=$(cd ${0%/*} && pwd) 6 | 7 | RUNNER_BASE_DIR=${RUNNER_SCRIPT_DIR%/*} 8 | RUNNER_ETC_DIR=$RUNNER_BASE_DIR/etc 9 | RUNNER_LOG_DIR=$RUNNER_BASE_DIR/log 10 | PIPE_DIR=/tmp/$RUNNER_BASE_DIR/ 11 | RUNNER_USER= 12 | 13 | # Make sure this script is running as the appropriate user 14 | if [ ! -z "$RUNNER_USER" ] && [ `whoami` != "$RUNNER_USER" ]; then 15 | exec sudo -u $RUNNER_USER -i $0 $@ 16 | fi 17 | 18 | # Make sure CWD is set to runner base dir 19 | cd $RUNNER_BASE_DIR 20 | 21 | # Make sure log directory exists 22 | mkdir -p $RUNNER_LOG_DIR 23 | 24 | # Extract the target node name from node.args 25 | NAME_ARG=`grep -e '-[s]*name' $RUNNER_ETC_DIR/vm.args` 26 | if [ -z "$NAME_ARG" ]; then 27 | echo "vm.args needs to have either -name or -sname parameter." 28 | exit 1 29 | fi 30 | 31 | # Extract the target cookie 32 | COOKIE_ARG=`grep -e '-setcookie' $RUNNER_ETC_DIR/vm.args` 33 | if [ -z "$COOKIE_ARG" ]; then 34 | echo "vm.args needs to have a -setcookie parameter." 35 | exit 1 36 | fi 37 | 38 | # Identify the script name 39 | SCRIPT=`basename $0` 40 | 41 | # Parse out release and erts info 42 | START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data` 43 | ERTS_VSN=${START_ERL% *} 44 | APP_VSN=${START_ERL#* } 45 | 46 | # Add ERTS bin dir to our path 47 | ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin 48 | 49 | # Setup command to control the node 50 | NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG" 51 | 52 | # Check the first argument for instructions 53 | case "$1" in 54 | start) 55 | # Make sure there is not already a node running 56 | RES=`$NODETOOL ping` 57 | if [ "$RES" = "pong" ]; then 58 | echo "Node is already running!" 59 | exit 1 60 | fi 61 | HEART_COMMAND="$RUNNER_BASE_DIR/bin/$SCRIPT start" 62 | export HEART_COMMAND 63 | mkdir -p $PIPE_DIR 64 | # Note the trailing slash on $PIPE_DIR/ 65 | $ERTS_PATH/run_erl -daemon $PIPE_DIR/ $RUNNER_LOG_DIR "exec $RUNNER_BASE_DIR/bin/$SCRIPT console" 2>&1 66 | ;; 67 | 68 | stop) 69 | # Wait for the node to completely stop... 70 | case `uname -s` in 71 | Linux|Darwin|FreeBSD|DragonFly|NetBSD|OpenBSD) 72 | # PID COMMAND 73 | PID=`ps ax -o pid= -o command=|\ 74 | grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'` 75 | ;; 76 | SunOS) 77 | # PID COMMAND 78 | PID=`ps -ef -o pid= -o args=|\ 79 | grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'` 80 | ;; 81 | CYGWIN*) 82 | # UID PID PPID TTY STIME COMMAND 83 | PID=`ps -efW|grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $2}'` 84 | ;; 85 | esac 86 | $NODETOOL stop 87 | while `kill -0 $PID 2>/dev/null`; 88 | do 89 | sleep 1 90 | done 91 | ;; 92 | 93 | restart) 94 | ## Restart the VM without exiting the process 95 | $NODETOOL restart 96 | ;; 97 | 98 | reboot) 99 | ## Restart the VM completely (uses heart to restart it) 100 | $NODETOOL reboot 101 | ;; 102 | 103 | ping) 104 | ## See if the VM is alive 105 | $NODETOOL ping 106 | ;; 107 | 108 | attach) 109 | # Make sure a node IS running 110 | RES=`$NODETOOL ping` 111 | if [ "$RES" != "pong" ]; then 112 | echo "Node is not running!" 113 | exit 1 114 | fi 115 | 116 | shift 117 | $ERTS_PATH/to_erl $PIPE_DIR 118 | ;; 119 | 120 | console) 121 | # Setup beam-required vars 122 | ROOTDIR=$RUNNER_BASE_DIR 123 | BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin 124 | EMU=beam 125 | PROGNAME=`echo $0 | sed 's/.*\\///'` 126 | CMD="$BINDIR/erlexec -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$SCRIPT -embedded -config $RUNNER_ETC_DIR/app.config -args_file $RUNNER_ETC_DIR/vm.args -- ${1+"$@"}" 127 | export EMU 128 | export ROOTDIR 129 | export BINDIR 130 | export PROGNAME 131 | 132 | # Dump environment info for logging purposes 133 | echo "Exec: $CMD" 134 | echo "Root: $ROOTDIR" 135 | 136 | # Log the startup 137 | logger -t "$SCRIPT[$$]" "Starting up" 138 | 139 | # Start the VM 140 | exec $CMD 141 | ;; 142 | 143 | *) 144 | echo "Usage: $SCRIPT {start|stop|restart|reboot|ping|console|attach}" 145 | exit 1 146 | ;; 147 | esac 148 | 149 | exit 0 150 | -------------------------------------------------------------------------------- /rel/files/services.config: -------------------------------------------------------------------------------- 1 | %% vim: ft=erlang 2 | %% Service configuration 3 | %% 4 | %% Format: {node, Nodename, [ServiceDef]} 5 | %% 6 | %% Service formats: 7 | %% 8 | %% - service 9 | %% Run service. 10 | %% 11 | %% - {service, [{option, value}]} 12 | %% Run service with custom options. 13 | %% 14 | %% - {service, name} 15 | %% Run service with custom name. 16 | %% 17 | %% - {service, name, [{option, value}]} 18 | %% Run service with custom name and options. 19 | 20 | {node, 'rolf@li153-242', [disk, 21 | {loadtime, [{url, "http://www.bbc.co.uk/"}]}, 22 | {sql, mysql_users, [{type, mysql}, 23 | {database, "mysql"}, 24 | {user, "root"}, 25 | {password, "xxxxxxxx"}, 26 | {query, "select count(*) as users from user"}, 27 | {metrics, [users]}]}]}. 28 | -------------------------------------------------------------------------------- /rel/files/vm.args: -------------------------------------------------------------------------------- 1 | 2 | ## Name of the node 3 | -name rolf@127.0.0.1 4 | 5 | ## Cookie for distributed erlang 6 | -setcookie rolf123 7 | -------------------------------------------------------------------------------- /rel/reltool.config: -------------------------------------------------------------------------------- 1 | %% vim: ft=erlang 2 | {sys, [{lib_dirs, ["../apps", "../deps"]}, 3 | {rel, "rolf", "0.1", [kernel, stdlib, sasl, log4erl, rolf]}, 4 | {rel, "start_clean", "", [kernel, stdlib]}, 5 | {boot_rel, "rolf"}, 6 | {profile, embedded}, 7 | {excl_sys_filters, ["^bin/.*", "^erts.*/bin/(dialyzer|typer)"]}, 8 | {app, sasl, [{incl_cond, include}]}, 9 | {app, log4erl, [{incl_cond, include}]}, 10 | {app, rolf, [{incl_cond, include}]}]}. 11 | 12 | {target_dir, "rolf"}. 13 | 14 | {overlay, [{mkdir, "data"}, 15 | {mkdir, "log"}, 16 | {copy, "files/erl", "{{erts_vsn}}/bin/erl"}, 17 | {copy, "files/nodetool", "{{erts_vsn}}/bin/nodetool"}, 18 | {copy, "files/rolf", "bin/rolf"}, 19 | {copy, "files/app.config", "etc/app.config"}, 20 | {copy, "files/vm.args", "etc/vm.args"}, 21 | {copy, "files/services.config", "etc/services.config"}, 22 | {copy, "files/log4erl.conf", "etc/log4erl.conf"}, 23 | {copy, "files/plugins", "plugins"}]}. 24 | --------------------------------------------------------------------------------