mirror of
https://github.com/valitydev/cg_mon.git
synced 2024-11-06 00:45:20 +00:00
Read memory metrics of cgroups which current process in.
This commit is contained in:
commit
5646ed4828
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ebin/
|
||||||
|
.idea/
|
||||||
|
.eunit/
|
||||||
|
*.beam
|
||||||
|
*.iml
|
||||||
|
deps/
|
||||||
|
logs/
|
||||||
|
.rebar/
|
28
README.md
Normal file
28
README.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
cg_mon
|
||||||
|
======
|
||||||
|
|
||||||
|
Simple application to extend osmon functionality when using cgroups.
|
||||||
|
Now it only supports reading memory cgroup metrics and can't automatically detect where cgroups mounted.
|
||||||
|
|
||||||
|
Usage example.
|
||||||
|
--------------
|
||||||
|
```
|
||||||
|
1> ok = application:load(cg_mon).
|
||||||
|
ok
|
||||||
|
2> ok = application:set_env(cg_mon, update_interval, 2000). %% Update metrics each 2 seconds. 1 second by default.
|
||||||
|
ok
|
||||||
|
3> ok = application:set_env(cg_mon, cgroup_root, "/directory/where/cgroups/mounted"). %% /sys/fs/cgroup by default
|
||||||
|
ok
|
||||||
|
4> ok = application:start(cg_mon).
|
||||||
|
ok
|
||||||
|
5> cg_mem_sup:rss().
|
||||||
|
1941913600
|
||||||
|
6> cg_mem_sup:usage().
|
||||||
|
4880506880
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
To be done:
|
||||||
|
1. Add support for another cgroups such as cpuacct, blkio and so on.
|
||||||
|
2. Add event notifications (for example, about excessing some limit of memory usage).
|
||||||
|
3. Add documentation.
|
3
eunit_test_data/cgroup.discovery
Normal file
3
eunit_test_data/cgroup.discovery
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
1:memory:/foo
|
||||||
|
2:cpu,cpuacct:/bar
|
||||||
|
3:cpuset:/
|
1
eunit_test_data/memory/foo/memory.limit_in_bytes
Normal file
1
eunit_test_data/memory/foo/memory.limit_in_bytes
Normal file
@ -0,0 +1 @@
|
|||||||
|
2
|
7
eunit_test_data/memory/foo/memory.stat
Normal file
7
eunit_test_data/memory/foo/memory.stat
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
cache 1
|
||||||
|
rss 2
|
||||||
|
mapped_file 4
|
||||||
|
pgpgin 5
|
||||||
|
pgpgout 6
|
||||||
|
pgfault 7
|
||||||
|
pgmajfault 8
|
1
eunit_test_data/memory/foo/memory.usage_in_bytes
Normal file
1
eunit_test_data/memory/foo/memory.usage_in_bytes
Normal file
@ -0,0 +1 @@
|
|||||||
|
1
|
20
include/test_utils.hrl
Normal file
20
include/test_utils.hrl
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-ifdef(TEST).
|
||||||
|
|
||||||
|
-ifndef(TEST_UTILS_HRL).
|
||||||
|
-define(TEST_UTILS_HRL, ok).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
|
||||||
|
-define(
|
||||||
|
TEST_DATA_DIR(),
|
||||||
|
filename:join( code:lib_dir(cgmon), "eunit_test_data" )
|
||||||
|
).
|
||||||
|
|
||||||
|
-define(
|
||||||
|
TEST_FILE(FileName),
|
||||||
|
filename:join(?TEST_DATA_DIR(), FileName)
|
||||||
|
).
|
||||||
|
|
||||||
|
-endif.
|
||||||
|
-endif.
|
95
src/cg_mem_sup.erl
Normal file
95
src/cg_mem_sup.erl
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
|
||||||
|
-module(cg_mem_sup).
|
||||||
|
-author("Viacheslav V. Kovalev").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([
|
||||||
|
provided_resource/0,
|
||||||
|
key_value_sources/0,
|
||||||
|
single_value_sources/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
limit/0,
|
||||||
|
swlimit/0,
|
||||||
|
usage/0,
|
||||||
|
swusage/0,
|
||||||
|
cache/0,
|
||||||
|
rss/0,
|
||||||
|
rss_huge/0,
|
||||||
|
mapped_file/0,
|
||||||
|
pgpgin/0,
|
||||||
|
pgpgout/0,
|
||||||
|
swap/0,
|
||||||
|
writeback/0,
|
||||||
|
inactive_anon/0,
|
||||||
|
active_anon/0,
|
||||||
|
inactive_file/0,
|
||||||
|
active_file/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(PROVIDED_RESOURCE, memory).
|
||||||
|
-define(KEY_VALUE_SOURCES, ["memory.stat"]).
|
||||||
|
-define(SINGLE_VALUE_SOURCES, [
|
||||||
|
{limit, "memory.limit_in_bytes"},
|
||||||
|
{swlimit, "memory.memsw.limit_in_bytes"},
|
||||||
|
{usage, "memory.usage_in_bytes"},
|
||||||
|
{swusage, "memory.memsw.usage_in_bytes"}
|
||||||
|
]).
|
||||||
|
|
||||||
|
provided_resource() ->
|
||||||
|
?PROVIDED_RESOURCE.
|
||||||
|
|
||||||
|
key_value_sources() ->
|
||||||
|
?KEY_VALUE_SOURCES.
|
||||||
|
|
||||||
|
single_value_sources() ->
|
||||||
|
?SINGLE_VALUE_SOURCES.
|
||||||
|
|
||||||
|
usage() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, usage).
|
||||||
|
|
||||||
|
swusage() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, swusage).
|
||||||
|
|
||||||
|
limit() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, limit).
|
||||||
|
|
||||||
|
swlimit() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, swlimit).
|
||||||
|
|
||||||
|
cache() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, cache).
|
||||||
|
|
||||||
|
rss() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, rss).
|
||||||
|
|
||||||
|
rss_huge() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, rss_huge).
|
||||||
|
|
||||||
|
mapped_file() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, mapped_file).
|
||||||
|
|
||||||
|
pgpgin() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, pgpgin).
|
||||||
|
|
||||||
|
pgpgout() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, pgpgout).
|
||||||
|
|
||||||
|
swap() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, swap).
|
||||||
|
|
||||||
|
writeback() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, writeback).
|
||||||
|
|
||||||
|
inactive_anon() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, inactive_anon).
|
||||||
|
|
||||||
|
active_anon() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, active_anon).
|
||||||
|
|
||||||
|
inactive_file() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, inactive_file).
|
||||||
|
|
||||||
|
active_file() ->
|
||||||
|
cg_mon_reader:read_meter(?PROVIDED_RESOURCE, active_file).
|
14
src/cg_mon.app.src
Normal file
14
src/cg_mon.app.src
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{application, cg_mon,
|
||||||
|
[
|
||||||
|
{description, "Reads cgroup's metrics"},
|
||||||
|
{vsn, "0.0.1"},
|
||||||
|
{registered, []},
|
||||||
|
{applications, [
|
||||||
|
kernel,
|
||||||
|
stdlib
|
||||||
|
]},
|
||||||
|
{mod, { cg_mon_app, []}},
|
||||||
|
{env, [
|
||||||
|
{update_interval, 1000}
|
||||||
|
]}
|
||||||
|
]}.
|
170
src/cg_mon_app.erl
Normal file
170
src/cg_mon_app.erl
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
-module(cg_mon_app).
|
||||||
|
-author("Viacheslav V. Kovalev").
|
||||||
|
|
||||||
|
-behaviour(application).
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
|
||||||
|
-ifdef(TEST).
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
%% Application callbacks
|
||||||
|
-export([start/2,
|
||||||
|
stop/1]).
|
||||||
|
|
||||||
|
%% Supervisor callbacks
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
|
||||||
|
%% Api functions
|
||||||
|
-export([
|
||||||
|
update_interval/0,
|
||||||
|
update_interval/1,
|
||||||
|
metrics_table/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Api functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
update_interval() ->
|
||||||
|
{ok, Value} = application:get_env(cg_mon, update_interval),
|
||||||
|
Value.
|
||||||
|
|
||||||
|
update_interval(Interval) when is_integer(Interval), Interval > 0 ->
|
||||||
|
application:set_env(cg_mon, update_interval, Interval).
|
||||||
|
|
||||||
|
metrics_table() ->
|
||||||
|
cg_mon_metrics.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Application callbacks
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
start(_StartType, _StartArgs) ->
|
||||||
|
SupervisorArgs = [
|
||||||
|
cgroup_root(), cgroup_discovery_file(), handler_modules()
|
||||||
|
],
|
||||||
|
ets:new( metrics_table(), [public, named_table] ),
|
||||||
|
SupervisorStartRes = supervisor:start_link({local, cg_mon_sup}, cg_mon_app, SupervisorArgs),
|
||||||
|
case SupervisorStartRes of
|
||||||
|
{ok, Pid} ->
|
||||||
|
{ok, Pid};
|
||||||
|
_ ->
|
||||||
|
{error, cgroups_error}
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
stop(_State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
|
||||||
|
init([CgroupRoot, CgroupDiscovery, AvailableHandlers]) ->
|
||||||
|
SupFlags = {one_for_one, 5, 3600},
|
||||||
|
case cg_mon_lib:discover_cgroups( CgroupRoot, CgroupDiscovery ) of
|
||||||
|
{ok, CgroupsMap} ->
|
||||||
|
HandlersMap = orddict:from_list([
|
||||||
|
{HandlerModule:provided_resource(), HandlerModule}
|
||||||
|
|| HandlerModule <- AvailableHandlers
|
||||||
|
]),
|
||||||
|
Children =
|
||||||
|
lists:foldl(
|
||||||
|
fun({Resource, Path}, Acc) ->
|
||||||
|
case orddict:find(Resource, HandlersMap) of
|
||||||
|
{ok, HandlerModule} ->
|
||||||
|
[ child_spec(HandlerModule, Path) | Acc ];
|
||||||
|
_ ->
|
||||||
|
Acc
|
||||||
|
end
|
||||||
|
end, [], CgroupsMap
|
||||||
|
),
|
||||||
|
{ok, {SupFlags, Children}};
|
||||||
|
{error, _Reason} ->
|
||||||
|
ignore
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Internal functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
child_spec(Module, FullCgroupPath) ->
|
||||||
|
Id = {Module, reader},
|
||||||
|
Args = [
|
||||||
|
Module:provided_resource(),
|
||||||
|
Module:key_value_sources(),
|
||||||
|
Module:single_value_sources(),
|
||||||
|
FullCgroupPath
|
||||||
|
],
|
||||||
|
{Id,
|
||||||
|
{cg_mon_reader, start_link, Args},
|
||||||
|
permanent, 2000, worker, [cg_mon_reader]
|
||||||
|
}.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cgroup_root() ->
|
||||||
|
case application:get_env(cg_mon, cgroup_root) of
|
||||||
|
{ok, Value} when is_list(Value) ->
|
||||||
|
Value;
|
||||||
|
_ ->
|
||||||
|
cg_mon_lib:cgroup_root()
|
||||||
|
end.
|
||||||
|
|
||||||
|
cgroup_discovery_file() ->
|
||||||
|
case application:get_env(cg_mon, cgroup_discovery_file) of
|
||||||
|
{ok, Value} when is_list(Value) ->
|
||||||
|
Value;
|
||||||
|
_ ->
|
||||||
|
cg_mon_lib:discovery_file()
|
||||||
|
end.
|
||||||
|
|
||||||
|
handler_modules() ->
|
||||||
|
[cg_mem_sup].
|
||||||
|
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Unit tests
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
-ifdef(TEST).
|
||||||
|
|
||||||
|
-include_lib("cgmon/include/test_utils.hrl").
|
||||||
|
|
||||||
|
reader_spec_test_() ->
|
||||||
|
Res = init([
|
||||||
|
?TEST_DATA_DIR(), ?TEST_FILE("cgroup.discovery"),
|
||||||
|
[cg_mem_sup]
|
||||||
|
]),
|
||||||
|
ExpectedArgs = [
|
||||||
|
cg_mem_sup:provided_resource(),
|
||||||
|
cg_mem_sup:key_value_sources(),
|
||||||
|
cg_mem_sup:single_value_sources(),
|
||||||
|
?TEST_FILE("memory/foo") ++ "/"
|
||||||
|
],
|
||||||
|
ExpectedChildren = [
|
||||||
|
{{cg_mem_sup, reader}, {cg_mon_reader, start_link, ExpectedArgs},
|
||||||
|
permanent, 2000, worker, [cg_mon_reader]}
|
||||||
|
],
|
||||||
|
?_assertMatch(
|
||||||
|
{ok, {_, ExpectedChildren}},
|
||||||
|
Res
|
||||||
|
).
|
||||||
|
|
||||||
|
no_discovery_test_() ->
|
||||||
|
Res = init([?TEST_DATA_DIR(), ?TEST_FILE("nonexistent.file"), [cg_mem_sup]]),
|
||||||
|
[
|
||||||
|
?_assertEqual(ignore, Res)
|
||||||
|
].
|
||||||
|
|
||||||
|
-endif.
|
282
src/cg_mon_lib.erl
Normal file
282
src/cg_mon_lib.erl
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
|
||||||
|
-module(cg_mon_lib).
|
||||||
|
-author("Viacheslav V. Kovalev").
|
||||||
|
|
||||||
|
-ifdef(TEST).
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([
|
||||||
|
discovery_file/0,
|
||||||
|
cgroup_root/0,
|
||||||
|
discover_cgroups/0,
|
||||||
|
discover_cgroups/1,
|
||||||
|
discover_cgroups/2,
|
||||||
|
read_cgroups_metrics/5
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export_type([
|
||||||
|
cgroup_map/0,
|
||||||
|
metric_source/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-type cgroup_map() :: [{Resource :: atom(), FullPath :: string()}].
|
||||||
|
-type metric_source() :: StatFile :: string() | { Key :: atom(), ValueFile :: string()}.
|
||||||
|
|
||||||
|
|
||||||
|
-spec discovery_file() -> string().
|
||||||
|
discovery_file() ->
|
||||||
|
"/proc/self/cgroup".
|
||||||
|
|
||||||
|
-spec cgroup_root() -> string().
|
||||||
|
cgroup_root() ->
|
||||||
|
"/sys/fs/cgroup".
|
||||||
|
|
||||||
|
-spec discover_cgroups() ->
|
||||||
|
{ok, cgroup_map()}
|
||||||
|
| {error, Reason :: atom()}.
|
||||||
|
discover_cgroups() ->
|
||||||
|
discover_cgroups(cgroup_root(), discovery_file()).
|
||||||
|
|
||||||
|
-spec discover_cgroups(Root :: string()) ->
|
||||||
|
{ok, cgroup_map()}
|
||||||
|
| {error, Reason :: atom()}.
|
||||||
|
discover_cgroups(Root) ->
|
||||||
|
discover_cgroups(Root, discovery_file()).
|
||||||
|
|
||||||
|
-spec discover_cgroups(Root :: string(), FileName :: string()) ->
|
||||||
|
{ok, cgroup_map()}
|
||||||
|
| {error, Reason :: atom()}.
|
||||||
|
discover_cgroups(Root, FileName) ->
|
||||||
|
with_file(
|
||||||
|
fun(DiscoveryFile) ->
|
||||||
|
parse_discovery_file(DiscoveryFile, Root)
|
||||||
|
end,
|
||||||
|
FileName
|
||||||
|
).
|
||||||
|
|
||||||
|
|
||||||
|
-spec parse_discovery_file(File :: file:io_device(), CgroupRoot :: string()) ->
|
||||||
|
{ok, cgroup_map()}
|
||||||
|
| {error, Reason :: atom()}.
|
||||||
|
parse_discovery_file(File, CgroupRoot) ->
|
||||||
|
foldl_lines(
|
||||||
|
fun(Line, Acc) ->
|
||||||
|
[_, Resources, GroupName] = string:tokens(Line, ":"),
|
||||||
|
ResourcesList = string:tokens(Resources, ","),
|
||||||
|
RelativeName = fixup_group_name(GroupName),
|
||||||
|
FullName = filename:join([CgroupRoot, Resources]) ++ RelativeName,
|
||||||
|
lists:foldl(
|
||||||
|
fun(ResourceName, TmpAcc) ->
|
||||||
|
[ {list_to_atom(ResourceName), FullName} | TmpAcc]
|
||||||
|
end, Acc, ResourcesList
|
||||||
|
)
|
||||||
|
end, [], File
|
||||||
|
).
|
||||||
|
|
||||||
|
|
||||||
|
-spec with_file(fun( (File :: file:io_device()) -> any() ), Filename :: string()) ->
|
||||||
|
any().
|
||||||
|
with_file(Fun, FileName) ->
|
||||||
|
case file:open(FileName, [read]) of
|
||||||
|
{ok, File} ->
|
||||||
|
Result = Fun(File),
|
||||||
|
file:close(File),
|
||||||
|
Result;
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
-spec foldl_lines(fun((Line :: string(), Acc :: any()) -> any()), InitAcc :: any(), File :: file:io_device()) ->
|
||||||
|
any().
|
||||||
|
foldl_lines(Fun, Acc, File) ->
|
||||||
|
case file:read_line(File) of
|
||||||
|
{ok, Line} ->
|
||||||
|
NewAcc = Fun( string:strip(Line, right, 10), Acc ),
|
||||||
|
foldl_lines(Fun, NewAcc, File);
|
||||||
|
eof ->
|
||||||
|
{ok, Acc};
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec read_cgroups_metrics(
|
||||||
|
OutputTable :: ets:tid(),
|
||||||
|
Resource :: atom(),
|
||||||
|
FullCgroupPath :: string(),
|
||||||
|
KeyValueStore :: [ ],
|
||||||
|
SingleValueStore :: [ ]
|
||||||
|
) ->
|
||||||
|
[{ FailedSource :: metric_source(), ErrorReason :: atom() }].
|
||||||
|
read_cgroups_metrics(OutputTable, Resource, FullCgroupPath, KeyValueStore, SingleValueStore) ->
|
||||||
|
KeyValueErrors =
|
||||||
|
lists:foldl(
|
||||||
|
fun(KeyValueFile, Acc) ->
|
||||||
|
case read_stat_file(FullCgroupPath, KeyValueFile) of
|
||||||
|
{ok, KeyValueData} ->
|
||||||
|
[
|
||||||
|
ets:insert(OutputTable, {{Resource, list_to_atom(Key)}, Value})
|
||||||
|
|| {Key, Value} <- KeyValueData
|
||||||
|
],
|
||||||
|
Acc;
|
||||||
|
{error, Reason} ->
|
||||||
|
[{KeyValueFile, Reason} | Acc]
|
||||||
|
end
|
||||||
|
end, [], KeyValueStore
|
||||||
|
),
|
||||||
|
lists:foldl(
|
||||||
|
fun(Entry = {Key, SingleValueFile}, Acc) ->
|
||||||
|
FullFileName = filename:join(FullCgroupPath, SingleValueFile),
|
||||||
|
ReadResult =
|
||||||
|
with_file(
|
||||||
|
fun(File) ->
|
||||||
|
{ok, Line} = file:read_line(File),
|
||||||
|
{ok, list_to_integer( strip_line_ending(Line) )}
|
||||||
|
end,
|
||||||
|
FullFileName
|
||||||
|
),
|
||||||
|
case ReadResult of
|
||||||
|
{ok, Value} ->
|
||||||
|
ets:insert(OutputTable, {{Resource, Key}, Value}),
|
||||||
|
Acc;
|
||||||
|
{error, Reason} ->
|
||||||
|
[{Entry, Reason} | Acc]
|
||||||
|
end
|
||||||
|
end, KeyValueErrors, SingleValueStore
|
||||||
|
).
|
||||||
|
|
||||||
|
|
||||||
|
-spec read_stat_file(FullCgroupPath :: string(), StatFile :: string()) ->
|
||||||
|
[{Key :: string(), Value :: integer()}].
|
||||||
|
read_stat_file(FullCgroupPath, StatFile) ->
|
||||||
|
with_file(
|
||||||
|
fun(File) ->
|
||||||
|
foldl_lines(
|
||||||
|
fun(Line, Acc) ->
|
||||||
|
[Key, Value] = string:tokens(Line, " "),
|
||||||
|
[{Key, list_to_integer(Value)} | Acc]
|
||||||
|
end, [], File
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
filename:join(FullCgroupPath, StatFile)
|
||||||
|
).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-spec fixup_group_name(GroupName :: string()) ->
|
||||||
|
string().
|
||||||
|
fixup_group_name(GroupName) ->
|
||||||
|
case strip_line_ending(GroupName) of
|
||||||
|
Value = "/" -> Value;
|
||||||
|
Value -> Value ++ "/"
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec strip_line_ending(Line :: string()) ->
|
||||||
|
string().
|
||||||
|
strip_line_ending(Line) ->
|
||||||
|
string:strip(Line, right, 10).
|
||||||
|
|
||||||
|
%% ----------------------------------------------------------------------------
|
||||||
|
%% EUnit test cases
|
||||||
|
%% ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-ifdef(TEST).
|
||||||
|
|
||||||
|
-include_lib("cgmon/include/test_utils.hrl").
|
||||||
|
|
||||||
|
discovery_error_test() ->
|
||||||
|
Res = discover_cgroups(cgroup_root(), ?TEST_FILE("non-existent-file")),
|
||||||
|
?assertEqual(Res, {error, enoent}).
|
||||||
|
|
||||||
|
discovery_denied_test() ->
|
||||||
|
Res = discover_cgroups(cgroup_root(), "/root/some-not-permitted-file"),
|
||||||
|
?assertEqual(Res, {error, eacces}).
|
||||||
|
|
||||||
|
discovery_test_() ->
|
||||||
|
{ok, Res} = discover_cgroups(cgroup_root(), ?TEST_FILE("cgroup.discovery")),
|
||||||
|
[
|
||||||
|
?_assertEqual(4, length(Res)),
|
||||||
|
?_assertEqual("/sys/fs/cgroup/memory/foo/", proplists:get_value(memory, Res)),
|
||||||
|
?_assertEqual("/sys/fs/cgroup/cpu,cpuacct/bar/", proplists:get_value(cpu, Res)),
|
||||||
|
?_assertEqual("/sys/fs/cgroup/cpu,cpuacct/bar/", proplists:get_value(cpuacct, Res)),
|
||||||
|
?_assertEqual("/sys/fs/cgroup/cpuset/", proplists:get_value(cpuset, Res))
|
||||||
|
].
|
||||||
|
|
||||||
|
|
||||||
|
read_stat_file_error_test_() ->
|
||||||
|
[
|
||||||
|
?_assertEqual(
|
||||||
|
{error, enoent},
|
||||||
|
read_stat_file(?TEST_FILE("some/not/existed/"), "file.stat")
|
||||||
|
),
|
||||||
|
?_assertEqual(
|
||||||
|
{error, eacces},
|
||||||
|
read_stat_file("/root/some/not/permitted", "file.stat")
|
||||||
|
)
|
||||||
|
].
|
||||||
|
|
||||||
|
read_stat_file_test_() ->
|
||||||
|
{ok, Result} = read_stat_file(?TEST_FILE("memory/foo/"), "memory.stat"),
|
||||||
|
[
|
||||||
|
?_assertEqual(7, length(Result)),
|
||||||
|
?_assertEqual(1, proplists:get_value("cache", Result))
|
||||||
|
].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
read_cgroups_metrics_test_() ->
|
||||||
|
KeyValueSources = [
|
||||||
|
"memory.stat",
|
||||||
|
"unknown.stat"
|
||||||
|
],
|
||||||
|
SingleValueSources = [
|
||||||
|
{limit, "memory.limit_in_bytes"},
|
||||||
|
{usage, "memory.usage_in_bytes"},
|
||||||
|
{unknown_metric, "some.unknown_metric"}
|
||||||
|
],
|
||||||
|
{setup,
|
||||||
|
fun() -> ets:new(?MODULE, []) end,
|
||||||
|
fun(Table) -> ets:delete(Table) end,
|
||||||
|
fun(Table) ->
|
||||||
|
ReadResult =
|
||||||
|
read_cgroups_metrics(
|
||||||
|
Table, memory, ?TEST_FILE("memory/foo"),
|
||||||
|
KeyValueSources, SingleValueSources
|
||||||
|
),
|
||||||
|
ExpectedErrors = [
|
||||||
|
{{unknown_metric, "some.unknown_metric"}, enoent},
|
||||||
|
{"unknown.stat", enoent}
|
||||||
|
],
|
||||||
|
ExpectedMetrics =
|
||||||
|
[{{memory,pgpgout},6},
|
||||||
|
{{memory,pgfault},7},
|
||||||
|
{{memory,rss},2},
|
||||||
|
{{memory,mapped_file},4},
|
||||||
|
{{memory,pgmajfault},8},
|
||||||
|
{{memory,pgpgin},5},
|
||||||
|
{{memory,usage},1},
|
||||||
|
{{memory,cache},1},
|
||||||
|
{{memory,limit},2}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
?_assertEqual( lists:sort(ExpectedErrors), lists:sort(ReadResult) ),
|
||||||
|
?_assertEqual(length(ExpectedMetrics), length(ets:tab2list(Table))),
|
||||||
|
lists:map(
|
||||||
|
fun({Key, Value}) ->
|
||||||
|
?_assertEqual( [{Key, Value}], ets:lookup(Table, Key) )
|
||||||
|
end, ExpectedMetrics
|
||||||
|
)
|
||||||
|
|
||||||
|
]
|
||||||
|
end
|
||||||
|
}.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-endif.
|
98
src/cg_mon_reader.erl
Normal file
98
src/cg_mon_reader.erl
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
-module(cg_mon_reader).
|
||||||
|
-author("Viacheslav V. Kovalev").
|
||||||
|
|
||||||
|
-define(PROVIDED_RESOURCE, memory).
|
||||||
|
-define(KEY_VALUE_SOURCES, ["memory.stat"]).
|
||||||
|
-define(SINGLE_VALUE_SOURCES, []).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([
|
||||||
|
start_link/4,
|
||||||
|
read_meter/2,
|
||||||
|
read_meter/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
init/1,
|
||||||
|
handle_call/3,
|
||||||
|
handle_cast/2,
|
||||||
|
handle_info/2,
|
||||||
|
code_change/3,
|
||||||
|
terminate/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Api functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
start_link(ProvidedResource, KeyValueSources, SingleValueSources, FullCgroupPath) ->
|
||||||
|
gen_server:start_link(
|
||||||
|
?MODULE, {ProvidedResource, KeyValueSources, SingleValueSources, FullCgroupPath}, []
|
||||||
|
).
|
||||||
|
|
||||||
|
read_meter(Resource, Meter, DefaultValue) ->
|
||||||
|
case ets:lookup(cg_mon_app:metrics_table(), {Resource, Meter}) of
|
||||||
|
[] ->
|
||||||
|
DefaultValue;
|
||||||
|
[{_, Value}] ->
|
||||||
|
Value
|
||||||
|
end.
|
||||||
|
|
||||||
|
read_meter(Resource, Meter) ->
|
||||||
|
read_meter(Resource, Meter, undefined).
|
||||||
|
|
||||||
|
|
||||||
|
-record(
|
||||||
|
state, {
|
||||||
|
provided_resource,
|
||||||
|
key_value_sources,
|
||||||
|
single_value_sources,
|
||||||
|
full_cgroup_path
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
|
init({ProvidedResource, KeyValueSources, SingleValueSources, FullCgroupPath}) ->
|
||||||
|
error_logger:info_msg(
|
||||||
|
"Starting cg_mon_reader for resource ~p to read from ~p",
|
||||||
|
[ProvidedResource, FullCgroupPath]
|
||||||
|
),
|
||||||
|
erlang:send( self(), update ),
|
||||||
|
{ok, #state{
|
||||||
|
full_cgroup_path = FullCgroupPath,
|
||||||
|
provided_resource = ProvidedResource,
|
||||||
|
key_value_sources = KeyValueSources,
|
||||||
|
single_value_sources = SingleValueSources
|
||||||
|
}}.
|
||||||
|
|
||||||
|
handle_call(_, _, State) ->
|
||||||
|
{reply, ok, State}.
|
||||||
|
|
||||||
|
handle_cast(_, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
|
||||||
|
handle_info(
|
||||||
|
update,
|
||||||
|
#state{
|
||||||
|
full_cgroup_path = FullCgroupPath,
|
||||||
|
provided_resource = ProvidedResource,
|
||||||
|
key_value_sources = KeyValueSources,
|
||||||
|
single_value_sources = SingleValueSources
|
||||||
|
} = State
|
||||||
|
) ->
|
||||||
|
cg_mon_lib:read_cgroups_metrics(
|
||||||
|
cg_mon_app:metrics_table(), ProvidedResource, FullCgroupPath,
|
||||||
|
KeyValueSources, SingleValueSources
|
||||||
|
),
|
||||||
|
erlang:send_after( cg_mon_app:update_interval(), self(), update ),
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
handle_info(_, State) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
code_change(_, State, _) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
terminate(_, _) ->
|
||||||
|
ok.
|
||||||
|
|
76
test/cgmon_SUITE.erl
Normal file
76
test/cgmon_SUITE.erl
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
|
||||||
|
-module(cgmon_SUITE).
|
||||||
|
-author("Viacheslav V. Kovalev").
|
||||||
|
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([
|
||||||
|
all/0,
|
||||||
|
groups/0,
|
||||||
|
init_per_group/2,
|
||||||
|
end_per_group/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
%% Test cases
|
||||||
|
-export([
|
||||||
|
successful_start_stop/1,
|
||||||
|
no_discovery_file/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
[
|
||||||
|
{group, start_stop_tests}
|
||||||
|
].
|
||||||
|
|
||||||
|
groups() ->
|
||||||
|
[
|
||||||
|
{start_stop_tests, [sequence], [
|
||||||
|
successful_start_stop,
|
||||||
|
no_discovery_file
|
||||||
|
]},
|
||||||
|
{cg_mem_sup_tests, [sequence], [
|
||||||
|
update_memory_metrics
|
||||||
|
]}
|
||||||
|
].
|
||||||
|
|
||||||
|
|
||||||
|
init_per_group(start_stop_tests, Config) ->
|
||||||
|
CgroupRoot = ?config(data_dir, Config),
|
||||||
|
ok = application:load(cg_mon),
|
||||||
|
ok = application:set_env( cg_mon, update_interval, 200 ),
|
||||||
|
[ {cgroup_root, CgroupRoot} | Config ].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end_per_group(start_stop_tests, Config) ->
|
||||||
|
application:stop(cg_mon),
|
||||||
|
ok = application:unload(cg_mon),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
successful_start_stop(Config) ->
|
||||||
|
CgroupRoot = ?config(cgroup_root, Config),
|
||||||
|
CgroupDiscoveryFile = filename:join(CgroupRoot, "cgroup"),
|
||||||
|
ok = application:set_env(cg_mon, cgroup_root, CgroupRoot),
|
||||||
|
ok = application:set_env(cg_mon, cgroup_discovery_file, CgroupDiscoveryFile),
|
||||||
|
ok = application:start(cg_mon),
|
||||||
|
timer:sleep(100),
|
||||||
|
11 = cg_mem_sup:cache(),
|
||||||
|
12 = cg_mem_sup:rss(),
|
||||||
|
13 = cg_mem_sup:rss_huge(),
|
||||||
|
ok = application:stop(cg_mon).
|
||||||
|
|
||||||
|
no_discovery_file(Config) ->
|
||||||
|
CgroupRoot = ?config(cgroup_root, Config),
|
||||||
|
CgroupDiscoveryFile = filename:join(CgroupRoot, "non_existent_file"),
|
||||||
|
ok = application:set_env(cg_mon, cgroup_root, CgroupRoot),
|
||||||
|
ok = application:set_env(cg_mon, cgroup_discovery_file, CgroupDiscoveryFile),
|
||||||
|
{error, {cgroups_error, _}} = application:start(cg_mon).
|
||||||
|
|
||||||
|
|
||||||
|
|
3
test/cgmon_SUITE_data/cgroup
Normal file
3
test/cgmon_SUITE_data/cgroup
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
1:memory:/foo
|
||||||
|
2:cpu,cpuacct:/bar
|
||||||
|
3:cpuset:/
|
4
test/cgmon_SUITE_data/memory/foo/memory.stat
Normal file
4
test/cgmon_SUITE_data/memory/foo/memory.stat
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
cache 11
|
||||||
|
rss 12
|
||||||
|
rss_huge 13
|
||||||
|
swap 14
|
4
test/cgmon_SUITE_data/memory/memory.stat
Normal file
4
test/cgmon_SUITE_data/memory/memory.stat
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
cache 1
|
||||||
|
rss 2
|
||||||
|
rss_huge 3
|
||||||
|
swap 4
|
Loading…
Reference in New Issue
Block a user