diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..66406bc --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,2 @@ +( (erlang-mode . ((indent-tabs-mode . nil)) ) + ) diff --git a/.travis.yml b/.travis.yml index 075b529..114b3ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,13 @@ +sudo: false language: erlang script: "make ci" otp_release: + - 17.4 + - 17.3 - 17.1 - 17.0 + - R16B03-1 + - R16B03 - R16B02 - R16B01 - R16B diff --git a/Makefile b/Makefile index 7f8207d..44b3b96 100644 --- a/Makefile +++ b/Makefile @@ -43,4 +43,5 @@ clean_plt: rm -f $(EXOMETER_PLT) dialyzer: deps compile $(EXOMETER_PLT) - dialyzer -r ebin --plt $(EXOMETER_PLT) $(DIALYZER_OPTS) + dialyzer -r ebin --plt $(EXOMETER_PLT) $(DIALYZER_OPTS) | \ + fgrep -v -f ./dialyzer.ignore-warnings diff --git a/README.md b/README.md index 5ac2b48..37566a3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Copyright (c) 2014 Basho Technologies, Inc. All Rights Reserved. -__Version:__ Dec 3 2014 19:51:33 +__Version:__ Apr 17 2015 14:08:02 __Authors:__ Ulf Wiger ([`ulf.wiger@feuerlabs.com`](mailto:ulf.wiger@feuerlabs.com)), Magnus Feuer ([`magnus.feuer@feuerlabs.com`](mailto:magnus.feuer@feuerlabs.com)). @@ -474,6 +474,13 @@ Exometer Core defaults can be changed either through OTP application environment variables or through the use of Basho's `cuttlefish` ([`https://github.com/basho/cuttlefish`](https://github.com/basho/cuttlefish)). +__Note:__ Exometer Core will check both the `exometer` and the `exometer_core` +application environments. The `exometer` environment overrides the +`exometer_core` environment. However, if only `exometer_core` is used, any +`exometer` environment will simply be ignored. This is because of the +application controller: environment data is not loaded until the application +in question is loaded. + #### Configuring type - entry maps #### @@ -734,6 +741,7 @@ processing is complete. exometer_proc exometer_report exometer_report_lager +exometer_report_logger exometer_report_tty exometer_shallowtree exometer_slide diff --git a/dialyzer.ignore-warnings b/dialyzer.ignore-warnings new file mode 100644 index 0000000..a5a2adb --- /dev/null +++ b/dialyzer.ignore-warnings @@ -0,0 +1,2 @@ +exometer.erl:209: The variable _ can never match since previous clauses completely covered the type 'enabled' +exometer.erl:444: The variable _ can never match since previous clauses completely covered the type 'enabled' diff --git a/doc/README.md b/doc/README.md index 9c1c4e4..e212344 100644 --- a/doc/README.md +++ b/doc/README.md @@ -4,7 +4,7 @@ Copyright (c) 2014 Basho Technologies, Inc. All Rights Reserved. -__Version:__ Dec 3 2014 19:51:33 +__Version:__ Apr 17 2015 14:08:02 __Authors:__ Ulf Wiger ([`ulf.wiger@feuerlabs.com`](mailto:ulf.wiger@feuerlabs.com)), Magnus Feuer ([`magnus.feuer@feuerlabs.com`](mailto:magnus.feuer@feuerlabs.com)). @@ -474,6 +474,13 @@ Exometer Core defaults can be changed either through OTP application environment variables or through the use of Basho's `cuttlefish` ([`https://github.com/basho/cuttlefish`](https://github.com/basho/cuttlefish)). +__Note:__ Exometer Core will check both the `exometer` and the `exometer_core` +application environments. The `exometer` environment overrides the +`exometer_core` environment. However, if only `exometer_core` is used, any +`exometer` environment will simply be ignored. This is because of the +application controller: environment data is not loaded until the application +in question is loaded. + #### Configuring type - entry maps #### @@ -734,6 +741,7 @@ processing is complete. exometer_proc exometer_report exometer_report_lager +exometer_report_logger exometer_report_tty exometer_shallowtree exometer_slide diff --git a/doc/edoc-info b/doc/edoc-info index e671710..a78b3ee 100644 --- a/doc/edoc-info +++ b/doc/edoc-info @@ -5,6 +5,6 @@ exometer_cpu,exometer_duration,exometer_entry,exometer_folsom, exometer_folsom_monitor,exometer_function,exometer_histogram, exometer_igor,exometer_info,exometer_probe,exometer_proc, - exometer_report,exometer_report_lager,exometer_report_tty, - exometer_shallowtree,exometer_slide,exometer_slot_slide, - exometer_spiral,exometer_uniform,exometer_util]}. + exometer_report,exometer_report_lager,exometer_report_logger, + exometer_report_tty,exometer_shallowtree,exometer_slide, + exometer_slot_slide,exometer_spiral,exometer_uniform,exometer_util]}. diff --git a/doc/exometer.md b/doc/exometer.md index 2b3765b..04f9806 100644 --- a/doc/exometer.md +++ b/doc/exometer.md @@ -181,7 +181,7 @@ value() = any() ## Function Index ## -
aggregate/2Aggregate datapoints of matching entries.
create_entry/1
delete/1Delete the metric.
ensure/3Ensure that metric exists and is of given type.
find_entries/1Find metrics based on a name prefix pattern.
get_value/1Fetch the current value of the metric.
get_value/2
get_values/1
info/1Returns a list of info items for Metric, see info/2.
info/2Retrieves information about a metric.
new/2Equivalent to new(Name, Type, []).
new/3Create a new metrics entry.
propose/3Propose a new exometer entry (no entry actually created).
re_register/3Create a new metrics entry, overwrite any old entry.
register_application/0Equivalent to register_application(current_application()).
register_application/1Registers statically defined entries with exometer.
repair/1Delete and re-create an entry.
reset/1Reset the metric.
sample/1Tells the metric (mainly probes) to take a sample.
select/1Perform an ets:select() on the set of metrics.
select/2Perform an ets:select() with a Limit on the set of metrics.
select_cont/1Equivalent to ets:select(Cont).
select_count/1Corresponds to ets:select_count/1.
setopts/2Change options for the metric.
start/0Start exometer and dependent apps (for testing).
stop/0Stop exometer and dependent apps (for testing).
update/2Update the given metric with Value.
update_or_create/2Update existing metric, or create+update according to template.
update_or_create/4
+
aggregate/2Aggregate datapoints of matching entries.
create_entry/1
delete/1Delete the metric.
ensure/3Ensure that metric exists and is of given type.
find_entries/1Find metrics based on a name prefix pattern.
get_value/1Fetch the current value of the metric.
get_value/2
get_values/1
global_status/1
info/1Returns a list of info items for Metric, see info/2.
info/2Retrieves information about a metric.
new/2Equivalent to new(Name, Type, []).
new/3Create a new metrics entry.
propose/3Propose a new exometer entry (no entry actually created).
re_register/3Create a new metrics entry, overwrite any old entry.
register_application/0Equivalent to register_application(current_application()).
register_application/1Registers statically defined entries with exometer.
repair/1Delete and re-create an entry.
reset/1Reset the metric.
sample/1Tells the metric (mainly probes) to take a sample.
select/1Perform an ets:select() on the set of metrics.
select/2Perform an ets:select() with a Limit on the set of metrics.
select_cont/1Equivalent to ets:select(Cont).
select_count/1Corresponds to ets:select_count/1.
setopts/2Change options for the metric.
start/0Start exometer and dependent apps (for testing).
stop/0Stop exometer and dependent apps (for testing).
update/2Update the given metric with Value.
update_or_create/2Update existing metric, or create+update according to template.
update_or_create/4
@@ -337,6 +337,13 @@ get_value(Name::name(), DataPoint:: + +### global_status/1 ### + +`global_status(St) -> any()` + + ### info/1 ### diff --git a/doc/exometer_admin.md b/doc/exometer_admin.md index 87a82a7..6b1f50d 100644 --- a/doc/exometer_admin.md +++ b/doc/exometer_admin.md @@ -11,7 +11,7 @@ __Behaviours:__ [`gen_server`](gen_server.md).
auto_create_entry/1
code_change/3
delete_entry/1
demonitor/1
ensure/3
find_auto_template/1Convenience function for testing which template will apply to -Name.
handle_call/3
handle_cast/2
handle_info/2
init/1
load_defaults/0
load_predefined/0
make_patterns/2
monitor/2
new_entry/3
normalize_name/1
prefixes/1
preset_defaults/0
propose/3
re_register_entry/3
register_application/1
repair_entry/1
set_default/3Sets a default definition for a metric type, possibly using wildcards.
start_link/0
terminate/2
+Name.handle_call/3handle_cast/2handle_info/2init/1load_defaults/0load_predefined/0make_patterns/2monitor/2monitor/3new_entry/3normalize_name/1prefixes/1preset_defaults/0propose/3re_register_entry/3register_application/1repair_entry/1set_default/3Sets a default definition for a metric type, possibly using wildcards.start_link/0terminate/2 @@ -69,7 +69,7 @@ Convenience function for testing which template will apply to ### handle_call/3 ### -`handle_call(X1, From, S) -> any()` +`handle_call(Req, From, S) -> any()` @@ -121,6 +121,13 @@ Convenience function for testing which template will apply to `monitor(Name, Pid) -> any()` + + +### monitor/3 ### + +`monitor(Name, Pid, OnError) -> any()` + + ### new_entry/3 ### diff --git a/doc/exometer_alias.md b/doc/exometer_alias.md index ddba74e..1246da5 100644 --- a/doc/exometer_alias.md +++ b/doc/exometer_alias.md @@ -128,13 +128,20 @@ stat_map() = [{name(), [{dp(), < ## Function Index ## -
delete/1Delete an alias, if it exists in the registry.
get_value/1Resolve the given alias and return corresponding metric and value.
load/1Load a list of mappings between entry+datapoint pairs and aliases.
new/3Create a new alias.
prefix_foldl/3Fold (ascending order) over the aliases matching Prefix.
prefix_foldr/3Fold (descending order) over the aliases matching Prefix.
prefix_match/1List all aliases matching the given prefix.
regexp_foldl/3Fold (ascending order) over the aliases matching Regexp.
regexp_foldr/3Fold (descending order) over the aliases matching Regexp.
resolve/1Look up an alias in the registry and return corresponding mapping.
reverse_map/2List all aliases mapped to the given entry+datapoint pair(s).
start_link/0
unload/1Unload a list of mappings.
update/2Resolves the given alias and updates the corresponding entry (if any).
+
check_map/1
delete/1Delete an alias, if it exists in the registry.
get_value/1Resolve the given alias and return corresponding metric and value.
load/1Load a list of mappings between entry+datapoint pairs and aliases.
new/3Create a new alias.
prefix_foldl/3Fold (ascending order) over the aliases matching Prefix.
prefix_foldr/3Fold (descending order) over the aliases matching Prefix.
prefix_match/1List all aliases matching the given prefix.
regexp_foldl/3Fold (ascending order) over the aliases matching Regexp.
regexp_foldr/3Fold (descending order) over the aliases matching Regexp.
resolve/1Look up an alias in the registry and return corresponding mapping.
reverse_map/2List all aliases mapped to the given entry+datapoint pair(s).
start_link/0
unload/1Unload a list of mappings.
update/2Resolves the given alias and updates the corresponding entry (if any).
## Function Details ## + + +### check_map/1 ### + +`check_map(Map) -> any()` + + ### delete/1 ### diff --git a/doc/exometer_cache.md b/doc/exometer_cache.md index f88e802..b679075 100644 --- a/doc/exometer_cache.md +++ b/doc/exometer_cache.md @@ -10,7 +10,7 @@ __Behaviours:__ [`gen_server`](gen_server.md). ## Function Index ## -
code_change/3
delete/2
handle_call/3
handle_cast/2
handle_info/2
init/1
read/2
start_link/0
terminate/2
write/3
write/4
+
code_change/3
delete/2
delete_name/1
handle_call/3
handle_cast/2
handle_info/2
init/1
read/2
start_link/0
terminate/2
write/3
write/4
@@ -31,6 +31,13 @@ __Behaviours:__ [`gen_server`](gen_server.md). `delete(Name, DataPoint) -> any()` + + +### delete_name/1 ### + +`delete_name(Name) -> any()` + + ### handle_call/3 ### diff --git a/doc/exometer_overview.odp b/doc/exometer_overview.odp new file mode 100644 index 0000000..1b9636e Binary files /dev/null and b/doc/exometer_overview.odp differ diff --git a/doc/exometer_overview.png b/doc/exometer_overview.png new file mode 100644 index 0000000..cedd5e9 Binary files /dev/null and b/doc/exometer_overview.png differ diff --git a/doc/exometer_probe.md b/doc/exometer_probe.md index cb0a36f..8f3582b 100644 --- a/doc/exometer_probe.md +++ b/doc/exometer_probe.md @@ -603,15 +603,53 @@ process when it receives a message that is not recognized by the internal receive loop. + The implementation of `probe_handle_msg/2` shall return `{ok, NewState}`, where `NewState` contains the new state of the probe that reflects the processed message. + + + + +### Fault tolerance ### + + +Probes are supervised by the `exometer_admin` process, and can be restarted +after a crash. Restart parameters are provided via the option +`{restart, Params}`, where `Params` is a list of `{Frequency, Action}` +tuples. `Frequency` is either `{Count, MilliSecs}` or `'_`', and +the corresponding `Action :: restart | disable | delete` will be performed +if the frequency of restarts falls within the given limit. + + + +For example, `[{{3, 1000}, restart}]` will allow 3 restarts within a 1-second +window. The matching is performed from top to bottom, and the first matching +pattern is acted upon. If no matching pattern is found, the default action +is `delete`. A pattern `{'_', Action}` acts as a catch-all, and should +be put last in the list. + + + +It is also possible to specify `{{Count, '_'}, Action}`, which means +that a total of `Count` restarts is permitted, regardless of how far apart +they are. The count is reset after each restart. + + +Example: + +```erlang + + {restart, [{{3,1000}, restart}, % up to 3 restarts within 1 sec + {{4,'_'} , disable}, % a total of up to 4 restarts + {'_' , delete}]} % anything else +``` ## Function Index ## -
behaviour/0
delete/3
get_datapoints/3
get_value/3
get_value/4
new/3
reset/3
sample/3
setopts/3
update/4
+
behaviour/0
delete/3
get_datapoints/3
get_value/3
get_value/4
new/3
reset/3
restart/4
sample/3
setopts/3
update/4
@@ -667,6 +705,13 @@ that reflects the processed message. `reset(Name, Type, Pid) -> any()` + + +### restart/4 ### + +`restart(Name, Module, Error, SpawnOpts) -> any()` + + ### sample/3 ### diff --git a/doc/exometer_proc.md b/doc/exometer_proc.md index 770cd09..a4477ce 100644 --- a/doc/exometer_proc.md +++ b/doc/exometer_proc.md @@ -37,7 +37,7 @@ following messages: ## Function Index ## -
call/2Make a synchronous call to an exometer_proc process.
cast/2Send an asynchronous message to an exometer_proc process.
format_status/2
handle_system_msg/4
process_options/1Apply process_flag-specific options.
spawn_process/2Spawn an exometer_proc process.
stop/0Terminate probe process in an orderly way.
system_code_change/4
system_continue/3
system_terminate/4
+
call/2Make a synchronous call to an exometer_proc process.
cast/2Send an asynchronous message to an exometer_proc process.
format_status/2
handle_system_msg/4
process_options/1Apply process_flag-specific options.
spawn_process/2Spawn an exometer_proc process.
spawn_process/3
stop/0Terminate probe process in an orderly way.
system_code_change/4
system_continue/3
system_terminate/4
@@ -130,6 +130,13 @@ Spawn an `exometer_proc` process. This function sets up appropriate monitoring, and calls the function `F` which needs to initialize the probe and enter an event loop. (Note: `exometer_proc` processes are responsible for their own event loop). + + +### spawn_process/3 ### + +`spawn_process(Name, F, Opts) -> any()` + + ### stop/0 ### diff --git a/doc/exometer_report.md b/doc/exometer_report.md index 7a4cf40..2d08295 100644 --- a/doc/exometer_report.md +++ b/doc/exometer_report.md @@ -369,7 +369,7 @@ time_ms() = pos_integer()
add_reporter/2Add a reporter.
call_reporter/2Send a custom (synchronous) call to Reporter.
cast_reporter/2Send a custom (asynchronous) cast to Reporter.
delete_interval/2Delete a named interval.
disable_me/2Used by a reporter to disable itself.
disable_reporter/1Disable Reporter.
enable_reporter/1Enable Reporter.
get_intervals/1List the named intervals for Reporter.
list_metrics/0Equivalent to list_metrics([]).
list_metrics/1List all metrics matching Path, together with subscription status.
list_reporters/0List the name and pid of each known reporter.
list_subscriptions/1List all subscriptions for Reporter.
new_entry/1Called by exometer whenever a new entry is created.
remove_reporter/1Remove reporter and all its subscriptions.
remove_reporter/2Remove Reporter (non-blocking call).
restart_intervals/1Restart all named intervals, respecting specified delays.
set_interval/3Specify a named interval.
setopts/3Called by exometer when options of a metric entry are changed.
start_link/0Starts the server --------------------------------------------------------------------.
start_reporters/0
subscribe/4Equivalent to subscribe(Reporter, Metric, DataPoint, Interval, [], false).
subscribe/5Equivalent to subscribe(Reporter, Metric, DataPoint, Interval, Extra, -false).
subscribe/6Add a subscription to an existing reporter.
terminate_reporter/1
unsubscribe/3Equivalent to unsubscribe(Reporter, Metric, DataPoint, []).
unsubscribe/4Removes a subscription.
unsubscribe_all/2Removes all subscriptions related to Metric in Reporter.
+false).subscribe/6Add a subscription to an existing reporter.terminate_reporter/1trigger_interval/2Trigger a named interval.unsubscribe/3Equivalent to unsubscribe(Reporter, Metric, DataPoint, []).unsubscribe/4Removes a subscription.unsubscribe_all/2Removes all subscriptions related to Metric in Reporter. @@ -412,12 +412,19 @@ subscriptions to the reporter. `{intervals, [named_interval()]}` named_interval() :: {Name::atom(), Interval::pos_integer()} | {Name::atom(), Interval::time_ms(), delay()::time_ms()} +| {Name::atom(), 'manual'} Define named intervals. The name can be used by subscribers, so that all subsriptions for a given named interval will be reported when the interval triggers. An optional delay (in ms) can be given: this will cause the first interval to start in `Delay` milliseconds. When all intervals are named at the same time, the delay parameter can be used to achieve staggered -reporting. +reporting. If the interval is specified as +``` + 'manual +``` +', it will have +to be triggered manually using [`trigger_interval/2`](#trigger_interval-2). + ### call_reporter/2 ### @@ -656,10 +663,16 @@ set_interval(Reporter::reporter_name(), Name:: Specify a named interval. - See [`add_reporter/2`](#add_reporter-2) for a description of named intervals. The named interval is here specified as either `Time` (milliseconds) or -`{Time, Delay}`, where a delay in milliseconds is provided. +`{Time, Delay}`, where a delay in milliseconds is provided. It is also +specify an interval as +``` + 'manual +``` + +', indicating that the interval can +only be triggered manually via [`trigger_interval/2`](#trigger_interval-2). If the named interval exists, it will be replaced with the new definition. @@ -765,6 +778,28 @@ even if the metric cannot be read. `terminate_reporter(Reporter) -> any()` + + +### trigger_interval/2 ### + + +

+trigger_interval(Reporter::reporter_name(), Name::atom()) -> ok
+
+
+ + +Trigger a named interval. + + +This function is mainly used to trigger intervals defined as +``` + 'manual +``` +', +but can be used to trigger any named interval. If a named interval with +a specified time in milliseconds is triggered this way, it will effectively +be restarted, and will repeat as usual from that point on. ### unsubscribe/3 ### diff --git a/doc/exometer_report_logger.md b/doc/exometer_report_logger.md new file mode 100644 index 0000000..696742e --- /dev/null +++ b/doc/exometer_report_logger.md @@ -0,0 +1,271 @@ + + +# Module exometer_report_logger # +* [Description](#description) +* [Data Types](#types) +* [Function Index](#index) +* [Function Details](#functions) + + +Exometer report collector and logger. +__Behaviours:__ [`gen_server`](gen_server.md). + +__This module defines the `exometer_report_logger` behaviour.__
Required callback functions: `logger_init_input/1`, `logger_init_output/1`, `logger_handle_data/2`. + + +## Description ## + + + +This module implements a behavior for collecting reporting data and +handling it (logging to disk or ets, printing to tty, etc.) + + + +The logger has built-in support for receiving input via UDP, TCP or +internal Erlang messaging, as well as a plugin API for custom input +handling. Correspondingly, it has support for output to TTY or ets, as +well as a plugin API for custom output. + + + +An example of how the logger can be used can be found in +`test/exometer_test_udp_reporter.erl`, which implements a UDP-based +reporter as well as an input plugin and an output plugin. This reporter +is used by `test/exometer_report_SUITE.erl`. + + + +Loggers can be combined, e.g. by creating one logger that receives Erlang +messages, and other loggers that receive from different sources, prefix +their reports and pass them on to the first logger. + + + + +## Input plugins ## + + + +An input plugin is initiated by `Module:logger_init_input(State)`, where +`State` is whatever was given as a `state` option (default: `undefined`). +The function must create a process and return `{ok, Pid}`. `Pid` is +responsible for setting up whatever input channel is desired, and passes +on incoming data to the logger via Erlang messages `{plugin, Pid, Data}`. + + + + +## Output Chaining ## + + + +Each incoming data item is passed through the list of output operators. +Each output operator is able to modify the data (the `tty` and `ets` +operators leave the data unchanged). Output plugins receive the data +in `Module:logger_handle_data(Data, State)`, which must return +`{NewData, NewState}`. The state is private to the plugin, while `NewData` +will be passed along to the next output operator. + + + + +## Flow control ## + + + +The logger will handle flow control automatically for `udp` and `tcp` +inputs. If `{active,once}` or `{active, false}`, the logger will trigger +`{active, once}` each time it has handled an incoming message. +If `{active, N}`, it will "refill" the port each time it receives an +indication that it has become passive. + + +Input plugins create a process in `Module:logger_init_input/1`. This process +can mimick the behavior of Erlang ports by sending a `{plugin_passive, Pid}` +message to the logger. The logger will reply with a message, +`{plugin_active, N}`, where `N` is the value given by the `active` option. + + +## Data Types ## + + + + +### logger_info() ### + + + +

+logger_info() = {id, any()} | {input, proplist()} | {output, proplist()}
+
+ + + + + +### proplist() ### + + + +

+proplist() = [{atom(), any()}]
+
+ + + + +## Function Index ## + + +
info/0List active logger instances.
info/1Lists the settings of a given logger instance.
new/1Create a new logger instance.
start_link/1Start function for logger instance.
+ + + + +## Function Details ## + + + +### info/0 ### + + +

+info() -> [{pid(), [logger_info()]}]
+
+
+ + +List active logger instances. + + +This function lists the instances started via [`new/1`](#new-1), along with their +respective settings as nested property lists. + + +### info/1 ### + + +

+info(P::pid()) -> [logger_info()]
+
+
+ +Lists the settings of a given logger instance. + + +### new/1 ### + + +

+new(Options::[{id, any()} | {input, list()} | {output, list()}]) -> {ok, pid()}
+
+
+ + +Create a new logger instance. + + + +This function creates a logger process with the given input and output +parameters. + + + +* `{id, ID}` is mainly for documentation and simplifying identification +of instances returned by [`info/0`](#info-0). +* `{input, PropList}` specifies what the logger listens to. Only the first +`input` entry is regarded, but the option is mandatory. +* `{output, PropList}` specifies what the logger should to with received +data. Multiple `output` entries are allowed, and they will be processed +in the order given. + + + +Valid input options: + + + +* `{mode, udp | tcp | internal | plugin}` defines the protocol +* `{active, false | true | once | N}` provides flow control. Default: `true`. +* (mode-specific options) + + + +Valid output options: + + + +* `{mode, tty | ets | plugin | internal}` defines output types +* (output-specific options) + + + +Mode-specific options, `udp`: + + + +* `{port, integer()}` - UDP port number +* `{options, list()}` - Options to pass to [`gen_udp:open/2`](gen_udp.md#open-2) + + + +Mode-specific options, `tcp`: + + + +* `{port, integer()}` - TCP port number +* `{options, list()}` - Options to pass to [`gen_tcp:listen/2`](gen_tcp.md#listen-2) + + + +Mode-specific options, `tty`: + + + +* `{prefix, iolist()}` - Prefix string inserted before the data, which is +printed as-is (note that any delimiter would need to be part of the prefix) + + + +Mode-specific options, `ets`: +* `{table, ets:table()}` - Ets table identifier. If not specified, an +ordered-set table will be created by the logger process. The incoming +data will be inserted as `{erlang:now(), Data}`. + + + +Mode-specific options, `internal`: +* `{process, PidOrRegname}` specifies another logger instance, which is to +receive data from this logger (if used in output), or which is allowed +to send to this logger (if used in input). If no process is given for +input, any process can send data (on the form +`{exometer_report_logger, Pid, Data}`) to this logger. + + + +Mode-specific options, `plugin`: + + +* `{module, Module}` - name of callback module +(behaviour: `exometer_report_logger`) +* `{state, State}` - Passed as initial argument to +`Module:logger_init_input/1` or `Module:logger_init_output/1`, depending +on whether the plugin is specified as input or output. + + +### start_link/1 ### + + +

+start_link(Options::proplist()) -> {ok, pid()}
+
+
+ + +Start function for logger instance. + + +This function is the start function eventually called as a result from +[`new/1`](#new-1), but whereas `new/1` creates a supervised instance, this +function simply creates the process. It would normally not be used directly. diff --git a/doc/exometer_util.md b/doc/exometer_util.md index 293a5fe..65f3803 100644 --- a/doc/exometer_util.md +++ b/doc/exometer_util.md @@ -32,7 +32,7 @@ timestamp() = non_neg_integer()
clear_event_flag/2
drop_duplicates/1 -drop_duplicates/1 will drop all duplicate elements from a list of tuples identified by their first element.
get_datapoints/1
get_env/2
get_opt/3
get_statistics/3Calculate statistics from a sorted list of values.
get_statistics2/4
get_status/1
histogram/1
histogram/2
pick_items/2Pick values from specified positions in a sorted list of numbers.
report_type/3
set_call_count/2
set_call_count/3
set_event_flag/2
set_status/2
table/0
tables/0
test_event_flag/2
timestamp/0Generate a millisecond-resolution timestamp.
timestamp_to_datetime/1Convert timestamp to a regular datetime.
+drop_duplicates/1 will drop all duplicate elements from a list of tuples identified by their first element.ensure_all_started/1get_datapoints/1get_env/2get_opt/2get_opt/3get_statistics/3Calculate statistics from a sorted list of values.get_statistics2/4get_status/1histogram/1histogram/2perc/2pick_items/2Pick values from specified positions in a sorted list of numbers.report_type/3set_call_count/2set_call_count/3set_event_flag/2set_status/2table/0tables/0test_event_flag/2timestamp/0Generate a millisecond-resolution timestamp.timestamp_to_datetime/1Convert timestamp to a regular datetime. @@ -60,6 +60,17 @@ drop_duplicates(List0::[tuple()]) -> [tuple()] `drop_duplicates/1` will drop all duplicate elements from a list of tuples identified by their first element. Elements which are not tuples will be dropped as well. If called with a non-list argument, the argument is returned as is. + + +### ensure_all_started/1 ### + + +

+ensure_all_started(App::atom()) -> {ok, [atom()]} | {error, term()}
+
+
+ + ### get_datapoints/1 ### @@ -74,6 +85,13 @@ If called with a non-list argument, the argument is returned as is. `get_env(Key, Default) -> any()` + + +### get_opt/2 ### + +`get_opt(K, Opts) -> any()` + + ### get_opt/3 ### @@ -140,6 +158,13 @@ Fulpatchad med min/max av Magnus Feuer. `histogram(Values, DataPoints) -> any()` + + +### perc/2 ### + +`perc(P, Len) -> any()` + + ### pick_items/2 ### diff --git a/doc/overview.edoc b/doc/overview.edoc index 001e7e4..74b8322 100644 --- a/doc/overview.edoc +++ b/doc/overview.edoc @@ -423,6 +423,12 @@ Exometer Core defaults can be changed either through OTP application environment variables or through the use of Basho's `cuttlefish' ([https://github.com/basho/cuttlefish]). +Note: Exometer Core will check both the `exometer' and the `exometer_core' +application environments. The `exometer' environment overrides the +`exometer_core' environment. However, if only `exometer_core' is used, any +`exometer' environment will simply be ignored. This is because of the +application controller: environment data is not loaded until the application +in question is loaded. === Configuring type - entry maps === diff --git a/rebar.config b/rebar.config index fb94a4f..219614d 100644 --- a/rebar.config +++ b/rebar.config @@ -12,6 +12,7 @@ {lager, ".*", {git, "git://github.com/basho/lager.git", {tag,"2.0.3"}}}, {parse_trans, ".*", {git, "git://github.com/uwiger/parse_trans.git", {tag,"2.9"}}}, {folsom, "0.7.4p5", {git, "git://github.com/basho/folsom.git", {tag, "0.7.4p5"}}}, + {meck, ".*", {git, "git://github.com/basho/meck.git", {tag,"0.8.2"}}}, {setup, ".*", {git, "git://github.com/uwiger/setup.git", {tag,"1.4"}}} ]}. @@ -35,6 +36,8 @@ "https://github.com/Feuerlabs/exometer_core", "master"}} ]}. +{ct_extra_params, "-config test/ct.config"}. + {xref_checks, [ undefined_function_calls, diff --git a/src/exo_montest.erl b/src/exo_montest.erl index b70a466..0641c26 100644 --- a/src/exo_montest.erl +++ b/src/exo_montest.erl @@ -18,29 +18,29 @@ -export([copy_folsom/3]). -export([behaviour/0, - delete/3, - get_datapoints/3, - get_value/4, - new/3, - reset/3, - sample/3, - setopts/3, - update/4]). + delete/3, + get_datapoints/3, + get_value/4, + new/3, + reset/3, + sample/3, + setopts/3, + update/4]). behaviour() -> entry. copy_folsom(Name, Type, Opts) when is_tuple(Name) -> {tuple_to_list(Name), ad_hoc, [{folsom_name, Name}, - {module, ?MODULE}, - {type, Type} - | options(Type, Opts)]}; + {module, ?MODULE}, + {type, Type} + | options(Type, Opts)]}; copy_folsom(_, _, _) -> false. new(N, _, Opts) -> {ok, {proplists:get_value(type, Opts, unknown), - proplists:get_value(folsom_name, Opts, N)}}. + proplists:get_value(folsom_name, Opts, N)}}. update(_, Value, counter, {_, Name}) -> folsom_metrics:notify_existing_metric(Name, {inc,Value}, counter); diff --git a/src/exometer.erl b/src/exometer.erl index 114d857..8682c55 100644 --- a/src/exometer.erl +++ b/src/exometer.erl @@ -60,13 +60,15 @@ register_application/1 ]). +-export([global_status/1]). + -export([create_entry/1]). % called only from exometer_admin.erl %% Convenience function for testing -export([start/0, stop/0]). -export_type([name/0, type/0, options/0, status/0, behaviour/0, - entry/0, datapoint/0]). + entry/0, datapoint/0]). -compile(inline). @@ -90,8 +92,8 @@ %% @doc Start exometer and dependent apps (for testing). start() -> - lager:start(), - application:start(exometer_core). + {ok,_} = exometer_util:ensure_all_started(exometer_core), + ok. %% @doc Stop exometer and dependent apps (for testing). stop() -> @@ -201,40 +203,46 @@ ensure(Name, Type, Opts) when is_list(Name), is_list(Opts) -> %% corresponding callback module will be called. For a disabled metric, %% `ok' will be returned without any other action being taken. %% @end -update(Name, Value) when is_list(Name) -> +update(Name, Value) -> + case exometer_global:status() of + enabled -> update_(Name, Value); + _ -> ok + end. + +update_(Name, Value) when is_list(Name) -> case ets:lookup(Table = exometer_util:table(), Name) of - [#exometer_entry{status = Status} = E] - when ?IS_ENABLED(Status) -> - case E of - #exometer_entry{module = ?MODULE, type = counter} -> + [#exometer_entry{status = Status} = E] + when ?IS_ENABLED(Status) -> + case E of + #exometer_entry{module = ?MODULE, type = counter} -> ets:update_counter( Table, Name, {#exometer_entry.value, Value}); - #exometer_entry{module = ?MODULE, - type = fast_counter, ref = {M, F}} -> + #exometer_entry{module = ?MODULE, + type = fast_counter, ref = {M, F}} -> fast_incr(Value, M, F); - #exometer_entry{module = ?MODULE, type = gauge} -> - ets:update_element( - ?EXOMETER_ENTRIES, - Name, [{#exometer_entry.value, Value}]); - #exometer_entry{behaviour = probe, - type = T, ref = Pid} -> + #exometer_entry{module = ?MODULE, type = gauge} -> + ets:update_element( + ?EXOMETER_ENTRIES, + Name, [{#exometer_entry.value, Value}]); + #exometer_entry{behaviour = probe, + type = T, ref = Pid} -> exometer_probe:update(Name, Value, T, Pid); - #exometer_entry{module = M, behaviour = entry, - type = Type, ref = Ref} -> - M:update(Name, Value, Type, Ref) - end, - update_ok(Status, E, Value); + #exometer_entry{module = M, behaviour = entry, + type = Type, ref = Ref} -> + M:update(Name, Value, Type, Ref) + end, + update_ok(Status, E, Value); [] -> {error, not_found}; - _ -> - ok + _ -> + ok end. update_ok(St, #exometer_entry{name = Name}, Value) when St band 2#10 =:= 2#10 -> try exometer_event ! {updated, Name, Value} catch - _:_ -> ok + _:_ -> ok end; update_ok(_, _, _) -> ok. @@ -251,24 +259,24 @@ update_ok(_, _, _) -> %% @end update_or_create(Name, Value) -> case update(Name, Value) of - {error, not_found} -> - case exometer_admin:auto_create_entry(Name) of - ok -> - update(Name, Value); - Error -> - Error - end; - ok -> - ok + {error, not_found} -> + case exometer_admin:auto_create_entry(Name) of + ok -> + update(Name, Value); + Error -> + Error + end; + ok -> + ok end. update_or_create(Name, Value, Type, Opts) -> case update(Name, Value) of - {error, not_found} -> - ensure(Name, Type, Opts), - update(Name, Value); - ok -> - ok + {error, not_found} -> + ensure(Name, Type, Opts), + update(Name, Value); + ok -> + ok end. fast_incr(N, M, F) when N > 0 -> @@ -292,7 +300,7 @@ get_value(Name) when is_list(Name) -> get_value(Name, default). -spec get_value(name(), datapoint() | [datapoint()]) -> - {ok, value()} | {error, not_found}. + {ok, value()} | {error, not_found}. get_value(Name, DataPoint) when is_list(Name), is_integer(DataPoint) -> get_value(Name, [DataPoint]); @@ -317,21 +325,21 @@ get_value_(#exometer_entry{ status = Status }, _DataPoints) %% If the value is cached, see if we can find it. %% In the default case, call again with resolved data points. get_value_(#exometer_entry{cache = Cache } = E, - DataPoints) when Cache =/= 0 -> + DataPoints) when Cache =/= 0 -> get_cached_value_(E, DataPoints); get_value_(#exometer_entry{ module = ?MODULE, - type = Type} = E, default) + type = Type} = E, default) when Type == counter; Type == fast_counter; Type == gauge -> get_value_(E, exometer_util:get_datapoints(E)); get_value_(#exometer_entry{ module = ?MODULE, - type = counter} = E, DataPoints0) -> + type = counter} = E, DataPoints0) -> DataPoints = datapoints(DataPoints0, E), [get_ctr_datapoint(E, D) || D <- DataPoints]; get_value_(#exometer_entry{ module = ?MODULE, - type = gauge, name = Name}, DataPoints0) -> + type = gauge, name = Name}, DataPoints0) -> [E] = ets:lookup(?EXOMETER_ENTRIES, Name), DataPoints = datapoints(DataPoints0, E), [get_gauge_datapoint(E, D) || D <- DataPoints]; @@ -342,16 +350,16 @@ get_value_(#exometer_entry{module = ?MODULE, [get_fctr_datapoint(E, D) || D <- DataPoints ]; get_value_(#exometer_entry{behaviour = entry, - module = Mod, - name = Name, - type = Type, - ref = Ref} = E, DataPoints0) -> + module = Mod, + name = Name, + type = Type, + ref = Ref} = E, DataPoints0) -> Mod:get_value(Name, Type, Ref, datapoints(DataPoints0, E)); get_value_(#exometer_entry{behaviour = probe, - name = Name, - type = Type, - ref = Ref} = E, DataPoints0) -> + name = Name, + type = Type, + ref = Ref} = E, DataPoints0) -> exometer_probe:get_value(Name, Type, Ref, datapoints(DataPoints0, E)). @@ -360,21 +368,21 @@ get_cached_value_(E, default) -> get_cached_value_(E, exometer_util:get_datapoints(E)); get_cached_value_(#exometer_entry{name = Name, - cache = CacheTTL } = E, - DataPoints) -> + cache = CacheTTL } = E, + DataPoints) -> %% Dig through all the data points and check for cache hit. %% Store all cached KV pairs, and all keys that must be %% read and cached. { Cached, Uncached } = - lists:foldr(fun(DataPoint, {Cached1, Uncached1}) -> - case exometer_cache:read(Name, DataPoint) of - not_found -> - { Cached1, [DataPoint | Uncached1] }; - {_, Value } -> - { [{ DataPoint, Value } | Cached1], Uncached1 } - end - end, {[],[]}, DataPoints), + lists:foldr(fun(DataPoint, {Cached1, Uncached1}) -> + case exometer_cache:read(Name, DataPoint) of + not_found -> + { Cached1, [DataPoint | Uncached1] }; + {_, Value } -> + { [{ DataPoint, Value } | Cached1], Uncached1 } + end + end, {[],[]}, DataPoints), %% Go through all cache misses and retreive their actual values. Result = get_value_(E#exometer_entry { cache = 0 }, Uncached), @@ -402,20 +410,20 @@ delete(Name) when is_list(Name) -> %% @end sample(Name) when is_list(Name) -> case ets:lookup(exometer_util:table(), Name) of - [#exometer_entry{status = Status} = E] when ?IS_ENABLED(Status) -> - case E of - #exometer_entry{module = ?MODULE, type = counter} -> ok; - #exometer_entry{module = ?MODULE, type = fast_counter} -> ok; - #exometer_entry{behaviour = probe, - type = Type, - ref = Ref} -> - exometer_probe:sample(Name, Type, Ref), - ok; - #exometer_entry{behaviour = entry, - module = M, type = Type, ref = Ref} -> - M:sample(Name, Type, Ref) - end; - [_] -> disabled; + [#exometer_entry{status = Status} = E] when ?IS_ENABLED(Status) -> + case E of + #exometer_entry{module = ?MODULE, type = counter} -> ok; + #exometer_entry{module = ?MODULE, type = fast_counter} -> ok; + #exometer_entry{behaviour = probe, + type = Type, + ref = Ref} -> + exometer_probe:sample(Name, Type, Ref), + ok; + #exometer_entry{behaviour = entry, + module = M, type = Type, ref = Ref} -> + M:sample(Name, Type, Ref) + end; + [_] -> disabled; [] -> {error, not_found} end. @@ -430,44 +438,50 @@ sample(Name) when is_list(Name) -> %% the exometer entry, which can be recalled using {@link info/2}, and will %% indicate the time that has passed since the metric was last reset. %% @end -reset(Name) when is_list(Name) -> +reset(Name) -> + case exometer_global:status() of + enabled -> reset_(Name); + _ -> ok + end. + +reset_(Name) when is_list(Name) -> case ets:lookup(exometer_util:table(), Name) of - [#exometer_entry{status = Status} = E] when ?IS_ENABLED(Status) -> - case E of - #exometer_entry{module = ?MODULE, type = counter} -> - TS = exometer_util:timestamp(), - [ets:update_element(T, Name, [{#exometer_entry.value, 0}, - {#exometer_entry.timestamp, TS}]) - || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]], - ok; - #exometer_entry{module = ?MODULE, type = fast_counter, - ref = {M, F}} -> - TS = exometer_util:timestamp(), - set_call_count(M, F, true), - [ets:update_element(T, Name, [{#exometer_entry.timestamp, TS}]) - || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]], - ok; - #exometer_entry{module = ?MODULE, type = gauge} -> - TS = exometer_util:timestamp(), - [ets:update_element(T, Name, [{#exometer_entry.value, 0}, - {#exometer_entry.timestamp, TS}]) - || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]], - ok; - #exometer_entry{behaviour = probe, type = Type, ref = Ref} -> - [exometer_cache:delete(Name, DataPoint) || - DataPoint <- exometer_util:get_datapoints(E)], - exometer_probe:reset(Name, Type, Ref), - ok; - #exometer_entry{behaviour = entry, - module = M, type = Type, ref = Ref} -> - [exometer_cache:delete(Name, DataPoint) || - DataPoint <- exometer_util:get_datapoints(E)], - M:reset(Name, Type, Ref) - end; - [] -> + [#exometer_entry{status = Status} = E] when ?IS_ENABLED(Status) -> + case E of + #exometer_entry{module = ?MODULE, type = counter} -> + TS = exometer_util:timestamp(), + [ets:update_element(T, Name, [{#exometer_entry.value, 0}, + {#exometer_entry.timestamp, TS}]) + || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]], + ok; + #exometer_entry{module = ?MODULE, type = fast_counter, + ref = {M, F}} -> + TS = exometer_util:timestamp(), + set_call_count(M, F, true), + [ets:update_element(T, Name, [{#exometer_entry.timestamp, TS}]) + || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]], + ok; + #exometer_entry{module = ?MODULE, type = gauge} -> + TS = exometer_util:timestamp(), + [ets:update_element(T, Name, [{#exometer_entry.value, 0}, + {#exometer_entry.timestamp, TS}]) + || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]], + ok; + #exometer_entry{behaviour = probe, type = Type, ref = Ref} -> + [exometer_cache:delete(Name, DataPoint) || + DataPoint <- exometer_util:get_datapoints(E)], + exometer_probe:reset(Name, Type, Ref), + ok; + #exometer_entry{behaviour = entry, + module = M, type = Type, ref = Ref} -> + [exometer_cache:delete(Name, DataPoint) || + DataPoint <- exometer_util:get_datapoints(E)], + M:reset(Name, Type, Ref) + end; + [] -> {error, not_found}; - _ -> - ok + _ -> + ok end. @@ -490,29 +504,29 @@ setopts(Name, Options) when is_list(Name), is_list(Options) -> [#exometer_entry{module = ?MODULE, type = Type, status = Status} = E] -> - if ?IS_DISABLED(Status) -> + if ?IS_DISABLED(Status) -> case lists:keyfind(status, 1, Options) of {_, enabled} -> - case Type of - fast_counter -> setopts_fctr(E, Options); - counter -> setopts_ctr(E, Options); - gauge -> setopts_gauge(E, Options) + case Type of + fast_counter -> setopts_fctr(E, Options); + counter -> setopts_ctr(E, Options); + gauge -> setopts_gauge(E, Options) end, reporter_setopts(E, Options, enabled); _ -> {error, disabled} end; - ?IS_ENABLED(Status) -> + ?IS_ENABLED(Status) -> case lists:keyfind(status, 1, Options) of {_, disabled} -> {_, Elems} = process_setopts(E, Options), update_entry_elems(Name, Elems), reporter_setopts(E, Options, disabled); R when R==false; R=={status,disabled} -> - case Type of - fast_counter -> setopts_fctr(E, Options); - counter -> setopts_ctr(E, Options); - gauge -> setopts_gauge(E, Options) + case Type of + fast_counter -> setopts_fctr(E, Options); + counter -> setopts_ctr(E, Options); + gauge -> setopts_gauge(E, Options) end, reporter_setopts(E, Options, enabled) end @@ -536,12 +550,31 @@ setopts(Name, Options) when is_list(Name), is_list(Options) -> {error, not_found} end. -module_setopts(#exometer_entry{behaviour = probe}=E, Options, NewStatus) -> +module_setopts(#exometer_entry{behaviour = probe, + name = Name, + status = St0} = E, Options, NewStatus) -> reporter_setopts(E, Options, NewStatus), - exometer_probe:setopts(E, Options, NewStatus); + case NewStatus of + disabled when ?IS_ENABLED(St0) -> + exometer_cache:delete_name(Name), + exometer_probe:stop_probe(E), + update_entry_elems(Name, [{#exometer_entry.ref, undefined}]); + enabled when ?IS_DISABLED(St0) -> + %% Previously, probes were not stopped when disabled + OldRef = E#exometer_entry.ref, + case is_pid(OldRef) andalso is_process_alive(OldRef) of + true -> ok; + false -> + {ok, Ref} = exometer_probe:start_probe(E), + update_entry_elems(Name, [{#exometer_entry.ref, Ref}]) + end; + _ -> + exometer_probe:setopts(E, Options, NewStatus) + end, + ok; module_setopts(#exometer_entry{behaviour = entry, - module=M} = E, Options, NewStatus) -> + module=M} = E, Options, NewStatus) -> case [O || {K, _} = O <- Options, not lists:member(K, [status, cache, ref])] of [] -> @@ -551,8 +584,8 @@ module_setopts(#exometer_entry{behaviour = entry, ok -> reporter_setopts(E, Options, NewStatus), ok; - E -> - E + {error,_} = Error -> + Error end end. @@ -626,25 +659,25 @@ info(#exometer_entry{} = E, Item) -> info(Name, Item) -> case ets:lookup(exometer_util:table(), Name) of [#exometer_entry{} = E] -> - info_(E, Item); + info_(E, Item); _ -> undefined end. info_(E, Item) -> case Item of - name -> E#exometer_entry.name; - type -> E#exometer_entry.type; - module -> E#exometer_entry.module; - value -> get_value_(E,[]); - cache -> E#exometer_entry.cache; - status -> info_status(E#exometer_entry.status); - timestamp -> E#exometer_entry.timestamp; - options -> E#exometer_entry.options; - ref -> E#exometer_entry.ref; - entry -> E; - datapoints-> exometer_util:get_datapoints(E); - _ -> undefined + name -> E#exometer_entry.name; + type -> E#exometer_entry.type; + module -> E#exometer_entry.module; + value -> get_value_(E,[]); + cache -> E#exometer_entry.cache; + status -> info_status(E#exometer_entry.status); + timestamp -> E#exometer_entry.timestamp; + options -> E#exometer_entry.options; + ref -> E#exometer_entry.ref; + entry -> E; + datapoints-> exometer_util:get_datapoints(E); + _ -> undefined end. info_status(S) when ?IS_ENABLED(S) -> @@ -666,12 +699,12 @@ datapoints(D, _) when is_list(D) -> info(Name) -> case ets:lookup(exometer_util:table(), Name) of [#exometer_entry{} = E0] -> - E = info_set_status(E0), + E = info_set_status(E0), Flds = record_info(fields, exometer_entry), lists:keyreplace(value, 1, lists:zip(Flds, tl(tuple_to_list(E))) ++ [ {datapoints, - exometer_util:get_datapoints(E)}], + exometer_util:get_datapoints(E)}], {value, get_value_(E, [])}); _ -> undefined @@ -699,8 +732,8 @@ find_entries(Path) -> [ { #exometer_entry{name = Pat, _ = '_'}, [], [{{ {element, #exometer_entry.name, '$_'}, {element, #exometer_entry.type, '$_'}, - status_body_pattern() - }}] } ]). + status_body_pattern() + }}] } ]). get_values(Path) -> Entries = find_entries(Path), @@ -781,9 +814,9 @@ select_cont(Cont) -> %% @end aggregate(Pattern, DataPoints) -> case aggr_select(Pattern) of - [] -> []; - Found -> - aggregate(Found, DataPoints, orddict:new()) + [] -> []; + Found -> + aggregate(Found, DataPoints, orddict:new()) end. aggr_select(Pattern) -> @@ -791,25 +824,47 @@ aggr_select(Pattern) -> aggregate([N|Ns], DPs, Acc) when is_list(N) -> case get_value(N, DPs) of - {ok, Vals} -> - aggregate(Ns, DPs, aggr_acc(Vals, Acc)); - _ -> - aggregate(Ns, DPs, Acc) + {ok, Vals} -> + aggregate(Ns, DPs, aggr_acc(Vals, Acc)); + _ -> + aggregate(Ns, DPs, Acc) end; aggregate([], _, Acc) -> Acc. aggr_acc([{D,V}|T], Acc) -> if is_integer(V) -> - aggr_acc(T, orddict:update(D, fun(Val) -> - Val + V - end, V, Acc)); + aggr_acc(T, orddict:update(D, fun(Val) -> + Val + V + end, V, Acc)); true -> - aggr_acc(T, Acc) + aggr_acc(T, Acc) end; aggr_acc([], Acc) -> Acc. +global_status(St) when St==enabled; St==disabled -> + Prev = exometer_global:status(), + if St =:= Prev -> ok; + true -> + parse_trans_mod:transform_module( + exometer_global, fun(Forms,_) -> pt(Forms, St) end, []) + end, + Prev. + +pt(Forms, St) -> + parse_trans:plain_transform( + fun(F) -> + plain_pt(F, St) + end, Forms). + +plain_pt({function, L, status, 0, [_]}, St) -> + {function, L, status, 0, + [{clause, L, [], [], [{atom, L, St}]}]}; +plain_pt(_, _) -> + continue. + + %% Perform variable replacement in the ets select pattern. %% We want to project the entries as a set of {Name, Type, Status} tuples. %% This means we need to perform variable substitution in both guards and @@ -864,27 +919,27 @@ g_subst(Ks) -> %% Potentially save Name, Type and Status match head patterns match_tail(N,T,S) -> case is_dollar(N) of true -> - [{N, {element,#exometer_entry.name,'$_'}}|match_tail(T,S)]; - false -> match_tail(T, S) + [{N, {element,#exometer_entry.name,'$_'}}|match_tail(T,S)]; + false -> match_tail(T, S) end. match_tail(T,S) -> case is_dollar(T) of true -> - [{T, {element,#exometer_entry.type,'$_'}}|match_tail(S)]; - false -> match_tail(S) + [{T, {element,#exometer_entry.type,'$_'}}|match_tail(S)]; + false -> match_tail(S) end. match_tail(S) -> case is_dollar(S) of true -> - [{S, status_element_pattern(S)}]; - false -> [] + [{S, status_element_pattern(S)}]; + false -> [] end. is_dollar(A) when is_atom(A) -> case atom_to_list(A) of - [$$ | Rest] -> - try _ = list_to_integer(Rest), true - catch error:_ -> false - end; - _ -> false + [$$ | Rest] -> + try _ = list_to_integer(Rest), true + catch error:_ -> false + end; + _ -> false end; is_dollar(_) -> false. @@ -892,8 +947,8 @@ is_dollar(_) -> false. g_subst_({_,_}=X) -> X; g_subst_(K) when is_atom(K) -> {K, {{{element,#exometer_entry.name,'$_'}, - {element,#exometer_entry.type,'$_'}, - status_element_pattern({element,#exometer_entry.status,'$_'})}}}. + {element,#exometer_entry.type,'$_'}, + status_element_pattern({element,#exometer_entry.status,'$_'})}}}. %% The status attribute: bit 1 indicates enabled (1) or disabled (0) @@ -904,7 +959,7 @@ status_element_pattern(S) -> status_body_pattern() -> {element, {'+',{'band', - {element,#exometer_entry.status,'$_'},1},1}, + {element,#exometer_entry.status,'$_'},1},1}, {{disabled,enabled}}}. p_subst(Ks) -> @@ -913,7 +968,7 @@ p_subst_({_,_}=X) -> X; p_subst_(K) when is_atom(K) -> {K, {{{element,#exometer_entry.name,'$_'}, {element,#exometer_entry.type,'$_'}, - status_body_pattern()}}}. + status_body_pattern()}}}. %% This function returns a list of elements for ets:update_element/3, %% to be used for updating the #exometer_entry{} instances. @@ -937,48 +992,48 @@ process_setopts_(#exometer_entry{options = OldOpts} = Entry, Options) -> fun ({cache, Val}, {#exometer_entry{cache = Cache0} = E, Elems} = Acc) -> - if is_integer(Val), Val >= 0 -> - if Val =/= Cache0 -> - {E#exometer_entry{cache = Val}, - add_elem(cache, Val, Elems)}; - true -> - Acc - end; - true -> error({illegal, {cache, Val}}) - end; + if is_integer(Val), Val >= 0 -> + if Val =/= Cache0 -> + {E#exometer_entry{cache = Val}, + add_elem(cache, Val, Elems)}; + true -> + Acc + end; + true -> error({illegal, {cache, Val}}) + end; ({status, Status}, {#exometer_entry{status = Status0} = E, Elems} = Acc) -> - case is_status_change(Status, Status0) of - {true, Status1} -> - {E#exometer_entry{status = Status1}, - add_elem(status, Status1, Elems)}; - false -> - Acc - end; - ({update_event, UE}, - {#exometer_entry{status = Status0} = E, Elems} = Acc) - when is_boolean(UE) -> - case (exometer_util:test_event_flag(update, Status0) == UE) of - true -> Acc; - false -> - %% value changed - if UE -> - Status = exometer_util:set_event_flag( - update, E#exometer_entry.status), - {E#exometer_entry{status = Status}, - add_elem(status, Status, Elems)}; - true -> - Status = exometer_util:clear_event_flag( - update, E#exometer_entry.status), - {E#exometer_entry{status = Status}, - add_elem(status, Status, Elems)} - end - end; - ({ref,R}, {E, Elems}) -> - {E#exometer_entry{ref = R}, add_elem(ref, R, Elems)}; + case is_status_change(Status, Status0) of + {true, Status1} -> + {E#exometer_entry{status = Status1}, + add_elem(status, Status1, Elems)}; + false -> + Acc + end; + ({update_event, UE}, + {#exometer_entry{status = Status0} = E, Elems} = Acc) + when is_boolean(UE) -> + case (exometer_util:test_event_flag(update, Status0) == UE) of + true -> Acc; + false -> + %% value changed + if UE -> + Status = exometer_util:set_event_flag( + update, E#exometer_entry.status), + {E#exometer_entry{status = Status}, + add_elem(status, Status, Elems)}; + true -> + Status = exometer_util:clear_event_flag( + update, E#exometer_entry.status), + {E#exometer_entry{status = Status}, + add_elem(status, Status, Elems)} + end + end; + ({ref,R}, {E, Elems}) -> + {E#exometer_entry{ref = R}, add_elem(ref, R, Elems)}; ({_,_}, Acc) -> - Acc - end, {Entry, []}, Options), - {E1, [{#exometer_entry.options, update_opts(Options, OldOpts)}|Elems]}. + Acc + end, {Entry, []}, Options), + {E1, [{#exometer_entry.options, update_opts(Options, OldOpts)}|Elems]}. is_status_change(enabled, St) -> if ?IS_ENABLED(St) -> false; @@ -1020,13 +1075,22 @@ type_arg_first(Opts) -> %% Retrieve individual data points for the counter maintained by %% the exometer record itself. get_ctr_datapoint(#exometer_entry{name = Name}, value) -> - {value, lists:sum([ets:lookup_element(T, Name, #exometer_entry.value) - || T <- exometer_util:tables()])}; + {value, counter_sum(Name)}; +get_ctr_datapoint(#exometer_entry{name = Name}, value16) -> + {value16, counter_sum(Name) rem 16#FFFF}; +get_ctr_datapoint(#exometer_entry{name = Name}, value32) -> + {value32, counter_sum(Name) rem 16#FFFFFFFF}; +get_ctr_datapoint(#exometer_entry{name = Name}, value64) -> + {value64, counter_sum(Name) rem 16#FFFFFFFFFFFFFFFF}; get_ctr_datapoint(#exometer_entry{timestamp = TS}, ms_since_reset) -> {ms_since_reset, exometer_util:timestamp() - TS}; get_ctr_datapoint(#exometer_entry{}, Undefined) -> {Undefined, undefined}. +counter_sum(Name) -> + lists:sum([ets:lookup_element(T, Name, #exometer_entry.value) + || T <- exometer_util:tables()]). + get_gauge_datapoint(#exometer_entry{value = Value}, value) -> {value, Value}; get_gauge_datapoint(#exometer_entry{timestamp = TS}, ms_since_reset) -> @@ -1054,7 +1118,7 @@ get_fctr_datapoint(#exometer_entry{ }, Undefined) -> create_entry(#exometer_entry{module = exometer, type = Type} = E) when Type == counter; - Type == gauge -> + Type == gauge -> E1 = E#exometer_entry{value = 0, timestamp = exometer_util:timestamp()}, insert_aliases(E1), [ets:insert(T, E1) || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]], @@ -1084,21 +1148,28 @@ create_entry(#exometer_entry{module = exometer, create_entry(#exometer_entry{module = Module, type = Type, name = Name, - options = Opts} = E) -> + status = Status, + options = Opts} = E) -> case - case Module:behaviour() of - probe -> - {probe, exometer_probe:new(Name, Type, [{ arg, Module} | Opts ]) }; - entry -> - {entry, Module:new(Name, Type, Opts) }; - - Other -> Other - end + case Module:behaviour() of + probe -> + if ?IS_ENABLED(Status) -> + {probe, exometer_probe:new( + Name, Type, [{ arg, Module} | Opts ])}; + ?IS_DISABLED(Status) -> + %% Don't start probe if disabled + {probe, ok} + end; + entry -> + {entry, Module:new(Name, Type, Opts) }; + + Other -> Other + end of {Behaviour, ok }-> insert_aliases(E), [ets:insert(T, E#exometer_entry { behaviour = Behaviour }) - || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]], + || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]], ok; {Behaviour, {ok, Ref}} -> insert_aliases(E), @@ -1140,10 +1211,10 @@ set_call_count(M, F, Bool) when is_atom(M), is_atom(F), is_boolean(Bool) -> %% register_application() -> case application:get_application() of - {ok, App} -> - register_application(App); - Other -> - {error, Other} + {ok, App} -> + register_application(App); + Other -> + {error, Other} end. -spec register_application(_Application::atom()) -> ok | error(). diff --git a/src/exometer_admin.erl b/src/exometer_admin.erl index cbe361e..c4d0a10 100644 --- a/src/exometer_admin.erl +++ b/src/exometer_admin.erl @@ -45,8 +45,9 @@ ]). -export([find_auto_template/1, prefixes/1, make_patterns/2]). --export([monitor/2, demonitor/1]). +-export([monitor/2, monitor/3, demonitor/1]). +-compile({no_auto_import, [monitor/3]}). -include("exometer.hrl"). -include("log.hrl"). @@ -99,20 +100,20 @@ load_predefined() -> register_application(App) -> %% Ignore if exometer is not running case whereis(exometer_admin) of - undefined -> ok; - _ -> - case setup:get_env(App, exometer_defaults) of - {ok, E} -> - do_load_defaults(App, get_predef(E)); - undefined -> - ok - end, - case setup:get_env(App, exometer_predefined) of - {ok, P} -> - do_load_predef(App, get_predef(P)); - undefined -> - ok - end + undefined -> ok; + _ -> + case setup:get_env(App, exometer_defaults) of + {ok, E} -> + do_load_defaults(App, get_predef(E)); + undefined -> + ok + end, + case setup:get_env(App, exometer_predefined) of + {ok, P} -> + do_load_predef(App, get_predef(P)); + undefined -> + ok + end end. get_predef({script, F} ) -> ok(file:script(F, []), F); @@ -124,36 +125,41 @@ do_load_defaults(Src, L) when is_list(L) -> lists:foreach( fun({NamePattern, Type, Spec}) -> try set_default(NamePattern, Type, Spec) - catch - error:E -> - lager:error("Defaults(~p): ERROR: ~p~n", [Src, E]) - end + catch + error:E -> + lager:error("Defaults(~p): ERROR: ~p~n", [Src, E]) + end end, L). do_load_predef(Src, L) when is_list(L) -> lists:foreach( fun({Name, Type, Options}) -> new_entry(Name, Type, Options); - ({delete, Key}) -> - predef_delete_entry(Key, Src); - ({re_register, {Name, Type, Options}}) -> - re_register_entry(Name, Type, Options); - ({select_delete, Pat}) -> - Found = exometer:select(Pat), - lists:foreach( - fun({K,_,_}) -> - predef_delete_entry(K, Src); - (Other) -> - lager:error("Predef(~p): ~p~n", - [Src, {bad_pattern,Other}]) - end, Found) + ({delete, Key}) -> + predef_delete_entry(Key, Src); + ({re_register, {Name, Type, Options}}) -> + re_register_entry(Name, Type, Options); + ({select_delete, Pat}) -> + Found = exometer:select(Pat), + lists:foreach( + fun({K,_,_}) -> + predef_delete_entry(K, Src); + (Other) -> + lager:error("Predef(~p): ~p~n", + [Src, {bad_pattern,Other}]) + end, Found); + ({aliases, Aliases}) -> + lists:foreach( + fun({Alias, Entry, DP}) -> + exometer_alias:new(Alias, Entry, DP) + end, Aliases) end, L). predef_delete_entry(Key, Src) -> case delete_entry(Key) of - ok -> ok; - Error -> - lager:error("Predef(~p): ~p~n", [Src, Error]) + ok -> ok; + Error -> + lager:error("Predef(~p): ~p~n", [Src, Error]) end. ok({ok, Res}, _) -> Res; @@ -189,10 +195,10 @@ re_register_entry(Name, Type, Opts) -> repair_entry(Name) -> case gen_server:call(?MODULE, {repair_entry, Name}) of - {error, Reason} -> - error(Reason); - ok -> - ok + {error, Reason} -> + error(Reason); + ok -> + ok end. delete_entry(Name) -> @@ -201,10 +207,10 @@ delete_entry(Name) -> ensure(Name, Type, Opts) -> {Type1, Opt1} = check_type_arg(Type, Opts), case gen_server:call(?MODULE, {ensure, Name, Type1, Opt1}) of - {error, Reason} -> - error(Reason); - ok -> - ok + {error, Reason} -> + error(Reason); + ok -> + ok end. auto_create_entry(Name) -> @@ -223,16 +229,19 @@ check_type_arg(Type, Opts) -> {Type, Opts}. monitor(Name, Pid) when is_pid(Pid) -> - gen_server:cast(?MODULE, {monitor, Name, Pid}). + monitor(Name, Pid, delete). + +monitor(Name, Pid, OnError) when is_pid(Pid) -> + gen_server:cast(?MODULE, {monitor, Name, Pid, OnError}). demonitor(Pid) when is_pid(Pid) -> gen_server:cast(?MODULE, {demonitor, Pid}). opts_to_rec(Type, Opts0) -> Opts = case lists:keymember(module, 1, Opts0) of - true -> Opts0; - false -> [{module, module(Type)}|Opts0] - end, + true -> Opts0; + false -> [{module, module(Type)}|Opts0] + end, Flds = record_info(fields, exometer_entry), lists:foldr(fun({K,V}, Acc) -> setelement(pos(K, Flds), Acc, V) @@ -256,7 +265,7 @@ start_link() -> init(_) -> {ok, #st{}}. -handle_call({new_entry, Name, Type, Opts, AllowExisting}, _From, S) -> +handle_call({new_entry, Name, Type, Opts, AllowExisting} = _Req, _From, S) -> try #exometer_entry{options = NewOpts} = E0 = lookup_definition(Name, Type, Opts), @@ -264,68 +273,78 @@ handle_call({new_entry, Name, Type, Opts, AllowExisting}, _From, S) -> case {ets:member(exometer_util:table(), Name), AllowExisting} of {true, false} -> {reply, {error, exists}, S}; - _ -> + _Other -> + lager:debug("_Other = ~p~n", [_Other]), E1 = process_opts(E0, NewOpts), - Res = exometer:create_entry(E1), - exometer_report:new_entry(E1), + Res = try exometer:create_entry(E1), + exometer_report:new_entry(E1) + catch + error:Error1 -> + lager:debug( + "ERROR create_entry(~p) :- ~p~n~p", + [E1, Error1, erlang:get_stacktrace()]), + erlang:error(Error1) + end, {reply, Res, S} end catch error:Error -> + lager:error("~p -*-> error:~p~n~p~n", + [_Req, Error, erlang:get_stacktrace()]), {reply, {error, Error}, S} end; handle_call({repair_entry, Name}, _From, S) -> try - #exometer_entry{} = E = exometer:info(Name, entry), - delete_entry_(Name), - exometer:create_entry(E), - {reply, ok, S} + #exometer_entry{} = E = exometer:info(Name, entry), + delete_entry_(Name), + exometer:create_entry(E), + {reply, ok, S} catch - error:Error -> - {reply, {error, Error}, S} + error:Error -> + {reply, {error, Error}, S} end; handle_call({propose, Name, Type, Opts}, _From, S) -> try - #exometer_entry{options = NewOpts} = E0 = - lookup_definition(Name, Type, Opts), - E1 = process_opts(E0, NewOpts), - {reply, exometer_info:pp(E1), S} + #exometer_entry{options = NewOpts} = E0 = + lookup_definition(Name, Type, Opts), + E1 = process_opts(E0, NewOpts), + {reply, exometer_info:pp(E1), S} catch - error:Error -> - {reply, {error, Error}, S} + error:Error -> + {reply, {error, Error}, S} end; handle_call({delete_entry, Name}, _From, S) -> {reply, delete_entry_(Name), S}; handle_call({ensure, Name, Type, Opts}, _From, S) -> case ets:lookup(exometer_util:table(), Name) of - [#exometer_entry{type = Type}] -> - {reply, ok, S}; - [#exometer_entry{type = _OtherType}] -> - {reply, {error, type_mismatch}, S}; - [] -> - #exometer_entry{options = OptsTemplate} = E0 = - lookup_definition(Name, Type, Opts), - E1 = process_opts(E0, OptsTemplate ++ Opts), - Res = exometer:create_entry(E1), - report_new_entry(E1), - {reply, Res, S} + [#exometer_entry{type = Type}] -> + {reply, ok, S}; + [#exometer_entry{type = _OtherType}] -> + {reply, {error, type_mismatch}, S}; + [] -> + #exometer_entry{options = OptsTemplate} = E0 = + lookup_definition(Name, Type, Opts), + E1 = process_opts(E0, OptsTemplate ++ Opts), + Res = exometer:create_entry(E1), + report_new_entry(E1), + {reply, Res, S} end; handle_call({auto_create, Name}, _From, S) -> case find_auto_template(Name) of - false -> - {reply, {error, no_template}, S}; - #exometer_entry{options = Opts} = E -> - E1 = process_opts(E#exometer_entry{name = Name}, Opts), - Res = exometer:create_entry(E1), - report_new_entry(E1), - {reply, Res, S} + false -> + {reply, {error, no_template}, S}; + #exometer_entry{options = Opts} = E -> + E1 = process_opts(E#exometer_entry{name = Name}, Opts), + Res = exometer:create_entry(E1), + report_new_entry(E1), + {reply, Res, S} end; handle_call(_, _, S) -> {reply, error, S}. -handle_cast({monitor, Name, Pid}, S) -> +handle_cast({monitor, Name, Pid, OnError}, S) -> Ref = erlang:monitor(process, Pid), - put(Ref, Name), + put(Ref, {Name, OnError}), put(Pid, Ref), {noreply, S}; handle_cast({demonitor, Pid}, S) -> @@ -345,14 +364,21 @@ handle_info({'DOWN', Ref, _, Pid, _}, S) -> case get(Ref) of undefined -> {noreply, S}; - Proc when is_atom(Proc) -> + {Name, OnError} when is_atom(Name); is_list(Name) -> + erase(Ref), + erase(Pid), + on_error(Name, OnError), + {noreply, S}; + Name when is_atom(Name) -> + %% BW compat erase(Ref), erase(Pid), {noreply, S}; Name when is_list(Name) -> + %% BW compat erase(Ref), erase(Pid), - try delete_entry_(Name) catch error:_ -> ok end, + on_error(Name, delete), {noreply, S} end; handle_info(_, S) -> @@ -363,17 +389,17 @@ terminate(_, _) -> code_change(_, S, _) -> case ets:info(?EXOMETER_REPORTERS, name) of - undefined -> create_reporter_tabs(); - _ -> ok + undefined -> create_reporter_tabs(); + _ -> ok end, {ok, S}. create_reporter_tabs() -> Heir = {heir, whereis(exometer_sup), []}, ets:new(?EXOMETER_REPORTERS, [public, named_table, set, - {keypos, 2}, Heir]), + {keypos, 2}, Heir]), ets:new(?EXOMETER_SUBS, [public, named_table, ordered_set, - {keypos, 2}, Heir]). + {keypos, 2}, Heir]). create_ets_tabs() -> @@ -385,10 +411,10 @@ create_ets_tabs() -> {keypos, 2}]), ets:new(?EXOMETER_ENTRIES, [public, named_table, ordered_set, {keypos, 2}]), - ets:new(?EXOMETER_REPORTERS, [public, named_table, set, - {keypos, 2}]), - ets:new(?EXOMETER_SUBS, [public, named_table, ordered_set, - {keypos, 2}]); + ets:new(?EXOMETER_REPORTERS, [public, named_table, set, + {keypos, 2}]), + ets:new(?EXOMETER_SUBS, [public, named_table, ordered_set, + {keypos, 2}]); _ -> true end. @@ -399,7 +425,47 @@ tables() -> %% ==== -lookup_definition(Name, ad_hoc, Opts) -> +on_error(Name, {restart, {M, F, A}}) -> + try call_restart(M, F, A) of + {ok, Ref} -> + if is_list(Name) -> + [ets:update_element(T, Name, [{#exometer_entry.ref, Ref}]) + || T <- exometer_util:tables()]; + true -> ok + end, + ok; + disable -> + try_disable_entry_(Name); + delete -> + try_delete_entry_(Name); + Other -> + restart_failed(Name, Other) + catch error:R -> + restart_failed(Name, {error, R}) + end, + ok; +on_error(Name, delete) -> + try_delete_entry_(Name); +on_error(_Proc, _OnError) -> + %% Not good, but will do for now. + lager:debug("Unrecognized OnError: ~p (~p)~n", [_OnError, _Proc]), + ok. + +call_restart(M, F, A) -> + apply(M, F, A). + +restart_failed(Name, Error) -> + lager:debug("Restart failed ~p: ~p~n", [Name, Error]), + if is_list(Name) -> + try_delete_entry_(Name); + true -> + ok + end. + +lookup_definition(Name, Type, Opts) -> + check_aliases(lookup_definition_(Name, Type, Opts)). + +lookup_definition_(Name, ad_hoc, Opts) -> case [K || K <- [module, type], not lists:keymember(K, 1, Opts)] of [] -> {E0, Opts1} = @@ -421,40 +487,49 @@ lookup_definition(Name, ad_hoc, Opts) -> [_|_] = Missing -> error({required, Missing}) end; -lookup_definition(Name, Type, Opts) -> +lookup_definition_(Name, Type, Opts) -> E = case ets:prev(?EXOMETER_SHARED, {default, Type, <<>>}) of - {default, Type, N} = D0 when N==[''], N==Name -> - case ets:lookup(?EXOMETER_SHARED, D0) of - [#exometer_entry{} = Def] -> - Def; - [] -> - default_definition_(Name, Type) - end; - {default, OtherType, _} when Type=/=OtherType -> - exometer_default(Name, Type); - '$end_of_table' -> - exometer_default(Name, Type); - _ -> - default_definition_(Name, Type) - end, + {default, Type, N} = D0 when N==[''], N==Name -> + case ets:lookup(?EXOMETER_SHARED, D0) of + [#exometer_entry{} = Def] -> + Def; + [] -> + default_definition_(Name, Type) + end; + {default, OtherType, _} when Type=/=OtherType -> + exometer_default(Name, Type); + '$end_of_table' -> + exometer_default(Name, Type); + _ -> + default_definition_(Name, Type) + end, merge_opts(Opts, E). merge_opts(Opts, #exometer_entry{options = DefOpts} = E) -> Opts1 = lists:foldl(fun({'--', Keys}, Acc) -> - case is_list(Keys) of - true -> - lists:foldl( - fun(K,Acc1) -> - lists:keydelete(K, 1, Acc1) - end, Acc, Keys); - false -> - error({invalid_option,'--'}) - end; - ({K,V}, Acc) -> - lists:keystore(K, 1, Acc, {K,V}) - end, DefOpts, Opts), + case is_list(Keys) of + true -> + lists:foldl( + fun(K,Acc1) -> + lists:keydelete(K, 1, Acc1) + end, Acc, Keys); + false -> + error({invalid_option,'--'}) + end; + ({K,V}, Acc) -> + lists:keystore(K, 1, Acc, {K,V}) + end, DefOpts, Opts), E#exometer_entry{options = Opts1}. +check_aliases(#exometer_entry{name = N, options = Opts} = E) -> + case lists:keyfind(aliases, 1, Opts) of + {_, Aliases} -> + exometer_alias:check_map([{N, Aliases}]); + _ -> + ok + end, + E. + default_definition_(Name, Type) -> case search_default(Name, Type) of #exometer_entry{} = E -> @@ -494,11 +569,11 @@ search_default(Name, Type) -> sort_defaults(L) -> lists:sort(fun comp_templates/2, - [E || #exometer_entry{type = T} = E <- L, - T =/= function andalso T =/= cpu]). + [E || #exometer_entry{type = T} = E <- L, + T =/= function andalso T =/= cpu]). comp_templates(#exometer_entry{name = {default, _, A}, type = Ta}, - #exometer_entry{name = {default, _, B}, type = Tb}) -> + #exometer_entry{name = {default, _, B}, type = Tb}) -> comp_names(A, B, Ta, Tb). @@ -521,10 +596,10 @@ comp_types(A, B) -> A =< B. %% @end find_auto_template(Name) -> case sort_defaults(ets:select(?EXOMETER_SHARED, - make_patterns('_', Name))) of - [] -> false; - [#exometer_entry{name = {default,_,['']}}|_] -> false; - [#exometer_entry{} = E|_] -> E + make_patterns('_', Name))) of + [] -> false; + [#exometer_entry{name = {default,_,['']}}|_] -> false; + [#exometer_entry{} = E|_] -> E end. make_patterns(Type, Name) when is_list(Name) -> @@ -568,34 +643,53 @@ process_opts(Entry, Options) -> %% Unknown option, pass on to exometer entry options list, replacing %% any earlier versions of the same option. ({cache, Val}, E) -> - if is_integer(Val), Val >= 0 -> - E#exometer_entry{cache = Val}; - true -> - error({illegal, {cache, Val}}) - end; + if is_integer(Val), Val >= 0 -> + E#exometer_entry{cache = Val}; + true -> + error({illegal, {cache, Val}}) + end; ({status, Status}, #exometer_entry{} = E) -> - if Status==enabled; Status==disabled -> - Status1 = exometer_util:set_status( - Status, Entry#exometer_entry.status), - E#exometer_entry{status = Status1}; - true -> - error({illegal, {status, Status}}) - end; - ({update_event, UE}, #exometer_entry{} = E) when is_boolean(UE) -> - if UE -> - Status = exometer_util:set_event_flag( - update, E#exometer_entry.status), - E#exometer_entry{status = Status}; - true -> - Status = exometer_util:clear_event_flag( - update, E#exometer_entry.status), - E#exometer_entry{status = Status} - end; + if Status==enabled; Status==disabled -> + Status1 = exometer_util:set_status( + Status, Entry#exometer_entry.status), + E#exometer_entry{status = Status1}; + true -> + error({illegal, {status, Status}}) + end; + ({update_event, UE}, #exometer_entry{} = E) when is_boolean(UE) -> + if UE -> + Status = exometer_util:set_event_flag( + update, E#exometer_entry.status), + E#exometer_entry{status = Status}; + true -> + Status = exometer_util:clear_event_flag( + update, E#exometer_entry.status), + E#exometer_entry{status = Status} + end; ({_Opt, _Val}, #exometer_entry{} = Entry1) -> - Entry1 - end, Entry#exometer_entry{options = Options}, Options). + Entry1 + end, Entry#exometer_entry{options = Options}, Options). + +try_disable_entry_(Name) when is_list(Name) -> + try exometer:setopts(Name, [{status, disabled}]) + catch + error:Err -> + lager:debug("Couldn't disable ~p: ~p~n", [Name, Err]), + try_delete_entry_(Name) + end; +try_disable_entry_(_Name) -> + ok. + +try_delete_entry_(Name) -> + try delete_entry_(Name) + catch + error:R -> + lager:debug("Couldn't delete ~p: ~p~n", [Name, R]), + ok + end. delete_entry_(Name) -> + exometer_cache:delete_name(Name), case ets:lookup(exometer_util:table(), Name) of [#exometer_entry{module = exometer, type = Type}] when Type==counter; Type==gauge -> @@ -604,34 +698,30 @@ delete_entry_(Name) -> ok; [#exometer_entry{module = exometer, type = fast_counter, ref = {M, F}}] -> - try - exometer_util:set_call_count(M, F, false) - after - [ets:delete(T, Name) || - T <- [?EXOMETER_ENTRIES|exometer_util:tables()]] - end, + try + exometer_util:set_call_count(M, F, false) + after + [ets:delete(T, Name) || + T <- [?EXOMETER_ENTRIES|exometer_util:tables()]] + end, ok; [#exometer_entry{behaviour = probe, - type = Type, ref = Ref} = E] -> - [ exometer_cache:delete(Name, DataPoint) || - DataPoint <- exometer_util:get_datapoints(E)], - try - exometer_probe:delete(Name, Type, Ref) - after - [ets:delete(T, Name) || - T <- [?EXOMETER_ENTRIES|exometer_util:tables()]] - end, - ok; + type = Type, ref = Ref}] -> + try + exometer_probe:delete(Name, Type, Ref) + after + [ets:delete(T, Name) || + T <- [?EXOMETER_ENTRIES|exometer_util:tables()]] + end, + ok; [#exometer_entry{module= Mod, behaviour = entry, - type = Type, ref = Ref} = E] -> - [ exometer_cache:delete(Name, DataPoint) || - DataPoint <- exometer_util:get_datapoints(E)], + type = Type, ref = Ref}] -> try Mod:delete(Name, Type, Ref) after [ets:delete(T, Name) || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]] end, - ok; + ok; [] -> {error, not_found} end. diff --git a/src/exometer_alias.erl b/src/exometer_alias.erl index c7178dd..bb15071 100644 --- a/src/exometer_alias.erl +++ b/src/exometer_alias.erl @@ -23,26 +23,27 @@ -behaviour(gen_server). -export([new/3, - load/1, - unload/1, - delete/1, - update/2, - resolve/1, + load/1, + unload/1, + check_map/1, + delete/1, + update/2, + resolve/1, reverse_map/2, - get_value/1, - prefix_match/1, - prefix_foldl/3, - prefix_foldr/3, - regexp_foldl/3, - regexp_foldr/3]). + get_value/1, + prefix_match/1, + prefix_foldl/3, + prefix_foldr/3, + regexp_foldl/3, + regexp_foldr/3]). -export([start_link/0, - init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -define(TAB, ?MODULE). -define(COMPILED_RE(P), is_tuple(P), element(1, P) == re_pattern). @@ -75,11 +76,13 @@ %% type, and returns `{error, exists}' if the alias has already been %% registered. %% @end -new(Alias, Entry, DP) when is_list(Entry), is_atom(DP), is_atom(Alias); - is_list(Entry), is_atom(DP), is_binary(Alias); - is_list(Entry), is_integer(DP), is_atom(Alias); - is_list(Entry), is_integer(DP), is_binary(Alias) -> - gen_server:call(?MODULE, {new, Alias, Entry, DP}). +new(Alias, Entry, DP) -> + case valid_arg(Alias, Entry, DP) of + true -> + gen_server:call(?MODULE, {new, Alias, Entry, DP}); + false -> + {error, invalid} + end. -spec load(fun(() -> stat_map())) -> ok. %% @doc Load a list of mappings between entry+datapoint pairs and aliases. @@ -120,10 +123,10 @@ delete(Alias) -> resolve(Alias) -> Key = to_key(Alias), case ets_lookup(Key) of - [#alias{entry = Entry, dp = DP}] -> - {Entry, DP}; - [] -> - error + [#alias{entry = Entry, dp = DP}] -> + {Entry, DP}; + [] -> + error end. -spec reverse_map(name() | '_', dp() | '_') -> [{alias(),name(),dp()}]. @@ -147,15 +150,15 @@ reverse_map(Name, Datapoint) -> %% @end get_value(Alias) -> case resolve(Alias) of - {Entry, DP} -> - case exometer:get_value(Entry, [DP]) of - {ok, [{_, Value}]} -> - {ok, Value}; - Error -> - Error - end; - error -> - {error, not_found} + {Entry, DP} -> + case exometer:get_value(Entry, [DP]) of + {ok, [{_, Value}]} -> + {ok, Value}; + Error -> + Error + end; + error -> + {error, not_found} end. -spec update(alias(), any()) -> ok | {error, any()}. @@ -167,10 +170,10 @@ get_value(Alias) -> %% @end update(Alias, Value) -> case resolve(Alias) of - {Entry, _} -> - exometer:update(Entry, Value); - error -> - {error, not_found} + {Entry, _} -> + exometer:update(Entry, Value); + error -> + {error, not_found} end. -spec prefix_match(binary()) -> [{alias(), name(), dp()}]. @@ -190,28 +193,28 @@ prefix_match(Pattern) when is_binary(Pattern) -> %% @end prefix_foldl(Prefix, F, Acc) -> case ets_lookup(Prefix) of - [] -> - prefix_foldl(ets_next(Prefix), Prefix, byte_size(Prefix), - F, Acc); - [#alias{key = Key}] -> - prefix_foldl(Key, Prefix, byte_size(Prefix), F, Acc) + [] -> + prefix_foldl(ets_next(Prefix), Prefix, byte_size(Prefix), + F, Acc); + [#alias{key = Key}] -> + prefix_foldl(Key, Prefix, byte_size(Prefix), F, Acc) end. prefix_foldl('$end_of_table', _, _, _, Acc) -> Acc; prefix_foldl(Key, Pattern, Sz, F, Acc) -> case Key of - <> -> - case ets_lookup(Key) of - [#alias{alias = Alias, entry = E, dp = DP}] -> - prefix_foldl(ets_next(Key), - Pattern, Sz, F, - F(Alias, E, DP, Acc)); - _ -> - prefix_foldl(ets_next(Key), Pattern, Sz, F, Acc) - end; - _ -> - Acc + <> -> + case ets_lookup(Key) of + [#alias{alias = Alias, entry = E, dp = DP}] -> + prefix_foldl(ets_next(Key), + Pattern, Sz, F, + F(Alias, E, DP, Acc)); + _ -> + prefix_foldl(ets_next(Key), Pattern, Sz, F, Acc) + end; + _ -> + Acc end. -spec prefix_foldr(binary(), fold_fun(), acc()) -> acc(). @@ -222,27 +225,27 @@ prefix_foldl(Key, Pattern, Sz, F, Acc) -> %% @end prefix_foldr(Pattern, F, Acc) -> case ets_lookup(Pattern) of - [] -> - prefix_foldr(ets_next(Pattern), Pattern, byte_size(Pattern), - F, Acc); - [#alias{key = Key}] -> - prefix_foldr(Key, Pattern, byte_size(Pattern), F, Acc) + [] -> + prefix_foldr(ets_next(Pattern), Pattern, byte_size(Pattern), + F, Acc); + [#alias{key = Key}] -> + prefix_foldr(Key, Pattern, byte_size(Pattern), F, Acc) end. prefix_foldr('$end_of_table', _, _, _, Acc) -> Acc; prefix_foldr(Key, Pattern, Sz, F, Acc) -> case Key of - <> -> - case ets_lookup(Key) of - [#alias{alias = Alias, entry = E, dp = DP}] -> - F(Alias, E, DP, prefix_foldr(ets_next(Key), - Pattern, Sz, F, Acc)); - _ -> - prefix_foldr(ets_next(Key), Pattern, Sz, F, Acc) - end; - _ -> - Acc + <> -> + case ets_lookup(Key) of + [#alias{alias = Alias, entry = E, dp = DP}] -> + F(Alias, E, DP, prefix_foldr(ets_next(Key), + Pattern, Sz, F, Acc)); + _ -> + prefix_foldr(ets_next(Key), Pattern, Sz, F, Acc) + end; + _ -> + Acc end. -spec regexp_foldl(regexp(), fold_fun(), acc()) -> acc(). @@ -260,35 +263,35 @@ regexp_foldl(Regexp, F, Acc) when ?COMPILED_RE(Regexp) -> regexp_foldl(Regexp, F, Acc) -> Prefix = regexp_prefix(Regexp), case ets_lookup(Prefix) of - [] -> - regexp_foldl(ets_next(Prefix), Prefix, byte_size(Prefix), - re_compile(Regexp), F, Acc); - [#alias{key = Key}] -> - regexp_foldl(Key, Prefix, byte_size(Prefix), - re_compile(Regexp), F, Acc) + [] -> + regexp_foldl(ets_next(Prefix), Prefix, byte_size(Prefix), + re_compile(Regexp), F, Acc); + [#alias{key = Key}] -> + regexp_foldl(Key, Prefix, byte_size(Prefix), + re_compile(Regexp), F, Acc) end. regexp_foldl('$end_of_table', _, _, _, _, Acc) -> Acc; regexp_foldl(Key, Prefix, Sz, Pattern, F, Acc) -> case Key of - <> -> - case re:run(Key, Pattern) of - {match, _} -> - case ets_lookup(Key) of - [#alias{alias = Alias, entry = E, dp = DP}] -> - regexp_foldl(ets_next(Key), Prefix, Sz, - Pattern, F, F(Alias, E, DP, Acc)); - _ -> - regexp_foldl(ets_next(Key), - Prefix, Sz, Pattern, F, Acc) - end; - nomatch -> - regexp_foldl(ets_next(Key), - Prefix, Sz, Pattern, F, Acc) - end; - _ -> - Acc + <> -> + case re:run(Key, Pattern) of + {match, _} -> + case ets_lookup(Key) of + [#alias{alias = Alias, entry = E, dp = DP}] -> + regexp_foldl(ets_next(Key), Prefix, Sz, + Pattern, F, F(Alias, E, DP, Acc)); + _ -> + regexp_foldl(ets_next(Key), + Prefix, Sz, Pattern, F, Acc) + end; + nomatch -> + regexp_foldl(ets_next(Key), + Prefix, Sz, Pattern, F, Acc) + end; + _ -> + Acc end. -spec regexp_foldr(regexp(), fold_fun(), acc()) -> acc(). @@ -306,37 +309,37 @@ regexp_foldr(Pattern, F, Acc) when ?COMPILED_RE(Pattern) -> regexp_foldr(Pattern, F, Acc) -> Prefix = regexp_prefix(Pattern), case ets_lookup(Prefix) of - [] -> - regexp_foldr(ets_next(Prefix), Prefix, byte_size(Prefix), - re_compile(Pattern), - F, Acc); - [#alias{key = Key}] -> - regexp_foldr(Key, Prefix, byte_size(Prefix), - re_compile(Pattern), F, Acc) + [] -> + regexp_foldr(ets_next(Prefix), Prefix, byte_size(Prefix), + re_compile(Pattern), + F, Acc); + [#alias{key = Key}] -> + regexp_foldr(Key, Prefix, byte_size(Prefix), + re_compile(Pattern), F, Acc) end. regexp_foldr('$end_of_table', _, _, _, _, Acc) -> Acc; regexp_foldr(Key, Prefix, Sz, Pattern, F, Acc) -> case Key of - <> -> - case re:run(Key, Pattern) of - {match, _} -> - case ets_lookup(Key) of - [#alias{alias = Alias, entry = E, dp = DP}] -> - F(Alias, E, DP, regexp_foldr(ets_next(Key), - Prefix, Sz, Pattern, - F, Acc)); - _ -> - regexp_foldr(ets_next(Key), Prefix, Sz, - Pattern, F, Acc) - end; - nomatch -> - regexp_foldr(ets_next(Key), Prefix, Sz, - Pattern, F, Acc) - end; - _ -> - Acc + <> -> + case re:run(Key, Pattern) of + {match, _} -> + case ets_lookup(Key) of + [#alias{alias = Alias, entry = E, dp = DP}] -> + F(Alias, E, DP, regexp_foldr(ets_next(Key), + Prefix, Sz, Pattern, + F, Acc)); + _ -> + regexp_foldr(ets_next(Key), Prefix, Sz, + Pattern, F, Acc) + end; + nomatch -> + regexp_foldr(ets_next(Key), Prefix, Sz, + Pattern, F, Acc) + end; + _ -> + Acc end. just_acc(Alias, Entry, DP, Acc) -> @@ -345,11 +348,11 @@ just_acc(Alias, Entry, DP, Acc) -> start_link() -> Tab = maybe_create_ets(), case gen_server:start_link({local, ?MODULE}, ?MODULE, [], []) of - {ok, Pid} = Res -> - ets:give_away(Tab, Pid, give_away), - Res; - Other -> - Other + {ok, Pid} = Res -> + ets:give_away(Tab, Pid, give_away), + Res; + Other -> + Other end. %% @private @@ -360,25 +363,25 @@ init(_) -> handle_call({new, Alias, Entry, DP}, _, St) -> Key = to_key(Alias), Res = case ets:member(?TAB, Key) of - true -> - {error, exists}; - false -> - ets:insert(?TAB, #alias{key = Key, alias = Alias, - entry = Entry, dp = DP}), - ok - end, + true -> + {error, exists}; + false -> + ets:insert(?TAB, #alias{key = Key, alias = Alias, + entry = Entry, dp = DP}), + ok + end, {reply, Res, St}; handle_call({load, F}, _, St) -> Res = try do_load(F) - catch - error:R -> {error, R} - end, + catch + error:R -> {error, R} + end, {reply, Res, St}; handle_call({unload, F}, _, St) -> Res = try do_unload(F) - catch - error:R -> {error, R} - end, + catch + error:R -> {error, R} + end, {reply, Res, St}; handle_call({delete, Alias}, _, St) -> Key = to_key(Alias), @@ -407,11 +410,11 @@ code_change(_, St, _) -> maybe_create_ets() -> case ets:info(?TAB, name) of - undefined -> - ets:new(?TAB, [ordered_set, named_table, public, - {keypos, #alias.key}, {heir, self(), failover}]); - _ -> - ?TAB + undefined -> + ets:new(?TAB, [ordered_set, named_table, public, + {keypos, #alias.key}, {heir, self(), failover}]); + _ -> + ?TAB end. ets_lookup(Key) -> ets:lookup(?TAB, Key). @@ -425,32 +428,112 @@ to_key(A) when is_binary(A) -> do_load(F) -> Map = F(), + check_map(Map), lists:foreach( fun({Entry, DPs}) when is_list(Entry), is_list(DPs) -> - lists:foreach( - fun({DP, Alias}) when is_atom(DP), is_atom(Alias); - is_atom(DP), is_binary(Alias) -> - Key = to_key(Alias), - ets:insert(?TAB, #alias{key = Key, - alias = Alias, - entry = Entry, - dp = DP}) - end, DPs) + lists:foreach( + fun({DP, Alias}) when is_atom(DP), is_atom(Alias); + is_atom(DP), is_binary(Alias) -> + Key = to_key(Alias), + true = ets:insert_new(?TAB, #alias{key = Key, + alias = Alias, + entry = Entry, + dp = DP}) + end, DPs) end, Map). +check_map(Map) -> + case lists:foldl( + fun(F, {M1,Es}) -> + F(M1, Es) + end, {Map, []}, [fun check_args/2, + fun check_duplicates/2, + fun check_existing/2]) of + {Map1, []} -> + Map1; + {_, Errors} -> + error({map_error, Errors}) + end. + +check_args(Map, Es) -> + Check = deep_fold( + fun({Alias, Entry, DP}, D) -> + case valid_arg(Alias, Entry, DP) of + true -> D; + false -> + orddict:append(Alias, {Entry, DP}, D) + end; + (Other, D) -> + orddict:append(unrecognized, Other, D) + end, orddict:new(), Map), + maybe_add_errors(Check, invalid, Map, Es). + +check_duplicates(Map, Es) -> + Check = deep_fold( + fun({Alias, Entry, DP}, D) -> + dict:append(Alias, {Entry,DP}, D); + (_, D) -> + D + end, dict:new(), Map), + %% We have duplicates if the value of any dict item is a list of length > 1. + Dups = dict:fold(fun(K, [_,_|_] = V, Acc) -> [{K, V}|Acc]; + (_, _, Acc) -> Acc + end, [], Check), + maybe_add_errors(Dups, duplicate_aliases, Map, Es). + +check_existing(Map, Es) -> + Check = deep_fold( + fun({Alias, Entry, DP}, Acc) -> + %% Accept identical entries + case resolve(Alias) of + {Entry, DP} -> Acc; + error -> Acc; + {OtherEntry, OtherDP} -> + orddict:append(Alias, {OtherEntry, OtherDP}, Acc) + end; + (_, D) -> + D + end, orddict:new(), Map), + maybe_add_errors(Check, existing_aliases, Map, Es). + +valid_arg(Alias, Entry, DP) + when is_list(Entry), is_atom(DP), is_atom(Alias); + is_list(Entry), is_atom(DP), is_binary(Alias); + is_list(Entry), is_integer(DP), is_atom(Alias); + is_list(Entry), is_integer(DP), is_binary(Alias) -> true; +valid_arg(_, _, _) -> + false. + + +maybe_add_errors([], _, Map, Es) -> {Map, Es}; +maybe_add_errors([_|_] = NewErrors, Kind, Map, Es) -> + {Map, [{Kind, NewErrors}|Es]}. + +deep_fold(F, Acc, Map) -> + lists:foldl( + fun({Entry, DPs}, Acc1) -> + lists:foldl( + fun({DP, Alias}, Acc2) -> + F({Alias, Entry, DP}, Acc2) + end, Acc1, DPs); + (Other, Acc1) -> + %% Bad input, but let's pass it on to the check function + F(Other, Acc1) + end, Acc, Map). + do_unload(F) -> Map = F(), lists:foreach( fun({Entry, DPs}) when is_list(Entry), is_list(DPs) -> - lists:foreach( - fun({DP, Alias}) when is_atom(Alias); + lists:foreach( + fun({DP, Alias}) when is_atom(Alias); is_binary(Alias) -> - Key = to_key(Alias), - ets:delete_object(?TAB, #alias{key = Key, + Key = to_key(Alias), + ets:delete_object(?TAB, #alias{key = Key, alias = Alias, entry = Entry, dp = DP}) - end, DPs) + end, DPs) end, Map). re_compile(R) -> @@ -482,11 +565,11 @@ regexp_prefix_(<<>>, Acc) -> alias_test_() -> {setup, fun() -> - exometer:start(), - create_entries(), - load_aliases(), - ets:tab2list(?TAB), - ok + exometer:start(), + create_entries(), + load_aliases(), + ets:tab2list(?TAB), + ok end, fun(_) -> application:stop(exometer) end, [?_test(t_resolve()), @@ -532,7 +615,7 @@ t_get_value() -> t_prefix_match() -> ?assertMatch( [{<>, [?Pfx,g,1 ],value}, - {<>, [?Pfx,g,10],value}], + {<>, [?Pfx,g,10],value}], prefix_match(<>)). t_prefix_match2() -> @@ -542,29 +625,29 @@ t_prefix_match2() -> t_prefix_foldl() -> ?assertMatch( [{<>, [?Pfx,g,10],value}, - {<>, [?Pfx,g,1 ],value}], + {<>, [?Pfx,g,1 ],value}], prefix_foldl(<>, - fun(A,E,D,Acc) -> [{A,E,D}|Acc] end, [])). + fun(A,E,D,Acc) -> [{A,E,D}|Acc] end, [])). t_regexp_foldl() -> ?assertMatch( [{<>,[?Pfx,g,5],value}, - {<>,[?Pfx,g,4],value}, - {<>,[?Pfx,g,3],value}], + {<>,[?Pfx,g,4],value}, + {<>,[?Pfx,g,3],value}], regexp_foldl(<<"^",?Pfx,"_g_[345]$">>, - fun(A,E,D,Acc) -> [{A,E,D}|Acc] end, [])). + fun(A,E,D,Acc) -> [{A,E,D}|Acc] end, [])). t_regexp_foldl2() -> ?assertMatch([], regexp_foldl(<<"^",?Pfx,"_g_[ab]$">>, - fun(A,E,D,Acc) -> [{A,E,D}|Acc] end, [])). + fun(A,E,D,Acc) -> [{A,E,D}|Acc] end, [])). t_regexp_foldr() -> ?assertMatch( [{<>,[?Pfx,g,3],value}, - {<>,[?Pfx,g,4],value}, - {<>,[?Pfx,g,5],value}], + {<>,[?Pfx,g,4],value}, + {<>,[?Pfx,g,5],value}], regexp_foldr(<<"^",?Pfx,"_g_[345]$">>, - fun(A,E,D,Acc) -> [{A,E,D}|Acc] end, [])). + fun(A,E,D,Acc) -> [{A,E,D}|Acc] end, [])). t_unload() -> ok = unload(fun test_aliases/0), @@ -590,7 +673,7 @@ load_aliases() -> test_aliases() -> [{[?Pfx,g,N], [{value, iolist_to_binary([?Pfx, "_g_", - integer_to_list(N)])}]} + integer_to_list(N)])}]} || N <- lists:seq(1,10)]. -endif. diff --git a/src/exometer_cache.erl b/src/exometer_cache.erl index eacc53a..620e8a2 100644 --- a/src/exometer_cache.erl +++ b/src/exometer_cache.erl @@ -16,7 +16,8 @@ -export([read/2, %% (Name, DataPoint) -> {ok, Value} | not_found write/3, %% (Name, DataPoint, Value) -> ok write/4, %% (Name, DataPoint, Value, TTL) -> ok - delete/2 + delete/2, + delete_name/1 ]). -export([init/1, @@ -63,6 +64,14 @@ delete(Name, DataPoint) -> %% Cancel the timer? ets:delete(?TABLE, path(Name, DataPoint)). +delete_name(Name) -> + %% This function currently doesn't cancel any timers. The cost of doing + %% so would outweigh the cost of reacting to any timers that happen to + %% fire (timer-based cleanup matches on TRef, so will not accidentally + %% delete the wrong entries.) + ets:match_delete(?TABLE, #cache{name = {Name,'_'}, _ = '_'}), + ok. + start_timer(Name, TTL, TS) -> gen_server:cast(?MODULE, {start_timer, Name, TTL, TS}). diff --git a/src/exometer_core.app.src b/src/exometer_core.app.src index f7f549a..12d349a 100644 --- a/src/exometer_core.app.src +++ b/src/exometer_core.app.src @@ -8,7 +8,8 @@ [ kernel, stdlib, - lager + lager, + setup ]}, {included_applications, [ diff --git a/src/exometer_core_sup.erl b/src/exometer_core_sup.erl index 9b910a8..4405c2c 100644 --- a/src/exometer_core_sup.erl +++ b/src/exometer_core_sup.erl @@ -1,6 +1,6 @@ %% ------------------------------------------------------------------- %% -%% Copyright (c) 2014 Basho Technologies, Inc. All Rights Reserved. +%% Copyright (c) 2014-15 Basho Technologies, Inc. All Rights Reserved. %% %% This Source Code Form is subject to the terms of the Mozilla Public %% License, v. 2.0. If a copy of the MPL was not distributed with this @@ -38,6 +38,7 @@ init([]) -> ?CHILD(exometer_admin, worker), ?CHILD(exometer_cache, worker), ?CHILD(exometer_report, worker), + ?CHILD(exometer_report_logger_sup, supervisor), ?CHILD(exometer_folsom_monitor, worker), ?CHILD(exometer_alias, worker) ], diff --git a/src/exometer_cpu.erl b/src/exometer_cpu.erl index 0c141b3..2f9001b 100644 --- a/src/exometer_cpu.erl +++ b/src/exometer_cpu.erl @@ -49,7 +49,7 @@ probe_init(_, _, Opts) -> probe_terminate(_) -> ok. probe_get_value(DPs, #st{data = Data0, - datapoints = DPs0} = S) -> + datapoints = DPs0} = S) -> Data1 = if Data0 == undefined -> sample(DPs0); true -> Data0 end, diff --git a/src/exometer_duration.erl b/src/exometer_duration.erl index e4d57ff..097995a 100644 --- a/src/exometer_duration.erl +++ b/src/exometer_duration.erl @@ -13,18 +13,18 @@ %% exometer_entry callbacks -export([new/3, - delete/3, - get_value/3, - get_value/4, - get_datapoints/3, - setopts/3, - update/4, - reset/3, - sample/3]). + delete/3, + get_value/3, + get_value/4, + get_datapoints/3, + setopts/3, + update/4, + reset/3, + sample/3]). %% exometer_probe callbacks -export([behaviour/0, - probe_init/3, + probe_init/3, probe_terminate/1, probe_get_value/2, probe_update/2, @@ -38,10 +38,10 @@ -include("exometer.hrl"). -import(netlink_stat, [get_value/1]). -record(st, {name, - t_start, - count = 0, - last = 0, - histogram, + t_start, + count = 0, + last = 0, + histogram, opts = []}). @@ -98,11 +98,11 @@ probe_get_datapoints(#st{histogram = H}) -> probe_get_value(DataPoints, #st{histogram = H} = St) -> case DataPoints -- ?DATAPOINTS of - [] -> - {ok, fill_datapoints(DataPoints, [], St)}; - HDPs -> - {ok, HVals} = exometer_histogram:probe_get_value(HDPs, H), - {ok, fill_datapoints(DataPoints, HVals, St)} + [] -> + {ok, fill_datapoints(DataPoints, [], St)}; + HDPs -> + {ok, HVals} = exometer_histogram:probe_get_value(HDPs, H), + {ok, fill_datapoints(DataPoints, HVals, St)} end. probe_setopts(_Entry, _Opts, _St) -> @@ -112,12 +112,12 @@ probe_update({timer_start, T}, St) -> {ok, St#st{t_start = T}}; probe_update({timer_end, T}, #st{histogram = H, count = C} = St) -> try - Duration = timer:now_diff(T, St#st.t_start), - {ok, H1} = exometer_histogram:probe_update(Duration, H), - {ok, St#st{histogram = H1, count = C+1, last = Duration}} + Duration = timer:now_diff(T, St#st.t_start), + {ok, H1} = exometer_histogram:probe_update(Duration, H), + {ok, St#st{histogram = H1, count = C+1, last = Duration}} catch - error:_ -> - {ok, St} + error:_ -> + {ok, St} end. probe_reset(#st{histogram = H} = St) -> diff --git a/src/exometer_folsom.erl b/src/exometer_folsom.erl index b1f1db4..5bffc26 100644 --- a/src/exometer_folsom.erl +++ b/src/exometer_folsom.erl @@ -12,7 +12,7 @@ -behaviour(exometer_entry). -export([behaviour/0, - new/3, + new/3, delete/3, get_datapoints/3, get_value/4, @@ -47,8 +47,8 @@ new(Name, duration, Opts) -> {folsom_metrics:new_duration(Name), opt_ref(Opts)}; new(Name, history, Opts) -> case lists:keyfind(size, 1, Opts) of - {_, Sz} -> {folsom_metrics:new_history(Name, Sz), opt_ref(Opts)}; - false -> {folsom_metrics:new_history(Name), opt_ref(Opts)} + {_, Sz} -> {folsom_metrics:new_history(Name, Sz), opt_ref(Opts)}; + false -> {folsom_metrics:new_history(Name), opt_ref(Opts)} end. @@ -81,30 +81,30 @@ reset(_, _, _) -> get_value(Name, history, _Ref, DataPoints0) -> try DataPoints = datapoints(history, DataPoints0), - lists:foldr( - fun(events, Acc) -> - [{events, just_events( - folsom_metrics_history:get_events(Name))} - | Acc]; - (values, Acc) -> - [{values, folsom_metrics_history:get_events(Name)} - | Acc]; - (timed_events, Acc) -> - [{timed_events, - timed_events( - folsom_metrics_history:get_events(Name))} - | Acc]; - (Sz, Acc) when is_integer(Sz), Sz > 0 -> - [{Sz, just_events( - folsom_metrics_history:get_events(Name, Sz))} - | Acc]; - (info, Acc) -> - [{info, folsom_metrics_history:get_value(Name)} - | Acc]; - (_, Acc) -> Acc - end, [], DataPoints) + lists:foldr( + fun(events, Acc) -> + [{events, just_events( + folsom_metrics_history:get_events(Name))} + | Acc]; + (values, Acc) -> + [{values, folsom_metrics_history:get_events(Name)} + | Acc]; + (timed_events, Acc) -> + [{timed_events, + timed_events( + folsom_metrics_history:get_events(Name))} + | Acc]; + (Sz, Acc) when is_integer(Sz), Sz > 0 -> + [{Sz, just_events( + folsom_metrics_history:get_events(Name, Sz))} + | Acc]; + (info, Acc) -> + [{info, folsom_metrics_history:get_value(Name)} + | Acc]; + (_, Acc) -> Acc + end, [], DataPoints) catch - error:_ -> unavailable + error:_ -> unavailable end; get_value(Name, Type, Ref, DataPoints) -> Trunc = get_trunc_opt(Ref), diff --git a/src/exometer_folsom_monitor.erl b/src/exometer_folsom_monitor.erl index b239234..a166552 100644 --- a/src/exometer_folsom_monitor.erl +++ b/src/exometer_folsom_monitor.erl @@ -24,12 +24,12 @@ -behaviour(gen_server). -export([start_link/0, - init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -export([monitor/2]). -export([hook/1]). @@ -78,9 +78,9 @@ start_link() -> %% @private init(_) -> Mon = lists:foldl( - fun({Mf, Mc}, D) -> - orddict:append(Mf, Mc, D) - end, orddict:new(), find_env()), + fun({Mf, Mc}, D) -> + orddict:append(Mf, Mc, D) + end, orddict:new(), find_env()), init_monitor(Mon), {ok, #st{mon = Mon}}. @@ -89,7 +89,7 @@ find_env() -> E2 = exometer_util:get_env(folsom_monitor, []), lists:flatmap( fun({_,_} = M) -> [M]; - (L) when is_list(L) -> L + (L) when is_list(L) -> L end, E1 ++ E2). %% @private @@ -120,20 +120,20 @@ init_monitor([_|_]) -> do_init_monitor() -> case is_transformed() of - true -> - lager:debug("already transformed...~n", []), - ok; - false -> - lager:debug("transforming folsom_metrics...~n", []), - parse_trans_mod:transform_module(folsom_metrics, fun pt/2, []) + true -> + lager:debug("already transformed...~n", []), + ok; + false -> + lager:debug("transforming folsom_metrics...~n", []), + parse_trans_mod:transform_module(folsom_metrics, fun pt/2, []) end. pt(Forms, _) -> Funcs = funcs(), NewForms = parse_trans:plain_transform( - fun(F) -> - plain_pt(F, Funcs) - end, Forms), + fun(F) -> + plain_pt(F, Funcs) + end, Forms), mark_transformed(NewForms). is_transformed() -> @@ -147,10 +147,10 @@ mark_transformed([H|T]) -> plain_pt({function,L,F,A,Cs}, Funcs) -> case lists:keyfind({F,A}, 1, Funcs) of - {_, Type} -> - {function,L,F,A,insert_hook(Type, Cs)}; - false -> - continue + {_, Type} -> + {function,L,F,A,insert_hook(Type, Cs)}; + false -> + continue end; plain_pt(_, _) -> continue. @@ -169,10 +169,10 @@ funcs() -> insert_hook(Type, Cs) -> lists:map( fun({clause,L0,Args,Gs,Body}) -> - L = element(2,hd(Body)), - {clause,L0,Args,Gs, - [{call,L,{remote,L,{atom,L,?MODULE},{atom,L,hook}}, - [cons([{atom,L,Type}|Args], L)]}|Body]} + L = element(2,hd(Body)), + {clause,L0,Args,Gs, + [{call,L,{remote,L,{atom,L,?MODULE},{atom,L,hook}}, + [cons([{atom,L,Type}|Args], L)]}|Body]} end, Cs). cons([H|T], L) -> {cons,L,H,cons(T,L)}; @@ -181,32 +181,32 @@ cons([] , L) -> {nil,L}. check_stack(Mon, Stack, Args) -> orddict:fold( fun('_', CBs, Acc) -> - _ = [maybe_create(CB, Args) || CB <- CBs], - Acc; - (Mod, CBs, Acc) -> - case lists:keymember(Mod, 1, Stack) of - true -> - _ = [maybe_create(CB, Args) || CB <- CBs]; - false -> - ignore - end, - Acc + _ = [maybe_create(CB, Args) || CB <- CBs], + Acc; + (Mod, CBs, Acc) -> + case lists:keymember(Mod, 1, Stack) of + true -> + _ = [maybe_create(CB, Args) || CB <- CBs]; + false -> + ignore + end, + Acc end, ok, Mon). maybe_create(CB, [FolsomType, Name | Args]) -> try CB:copy_folsom(Name, FolsomType, Args) of - {ExoName, ExoType, ExoArgs} -> - exometer:new(ExoName, ExoType, ExoArgs); - L when is_list(L) -> - lists:foreach( - fun({ExoName, ExoType, ExoArgs}) -> - exometer:new(ExoName, ExoType, ExoArgs) - end, L); - false -> - ignore + {ExoName, ExoType, ExoArgs} -> + exometer:new(ExoName, ExoType, ExoArgs); + L when is_list(L) -> + lists:foreach( + fun({ExoName, ExoType, ExoArgs}) -> + exometer:new(ExoName, ExoType, ExoArgs) + end, L); + false -> + ignore catch - Cat:Msg -> - lager:error("~p:copy_folsom(~p,~p,~p): ~p:~p~n", - [CB, Name, FolsomType, Args, Cat, Msg]), - ignore + Cat:Msg -> + lager:error("~p:copy_folsom(~p,~p,~p): ~p:~p~n", + [CB, Name, FolsomType, Args, Cat, Msg]), + ignore end. diff --git a/src/exometer_function.erl b/src/exometer_function.erl index b1fe197..07d9d24 100644 --- a/src/exometer_function.erl +++ b/src/exometer_function.erl @@ -13,7 +13,7 @@ -behaviour(exometer_entry). -export([behaviour/0, - new/3, + new/3, update/4, reset/3, get_value/4, @@ -45,11 +45,11 @@ arg_spec(), res_type(), datapoints()}. -type fun_spec() :: simple_fun() | extended_fun(). -type fun_rep() :: {mod_name(), fun_name()} - | {mod_name(), fun_name(), each | once, - arg_spec(), res_type(), datapoints()} - | {mod_name(), fun_name(), each | once, - arg_spec(), match, any()} - | {eval, [expr()], datapoints()}. + | {mod_name(), fun_name(), each | once, + arg_spec(), res_type(), datapoints()} + | {mod_name(), fun_name(), each | once, + arg_spec(), match, any()} + | {eval, [expr()], datapoints()}. -spec behaviour() -> exometer:behaviour(). behaviour() -> @@ -257,7 +257,7 @@ actual_datapoints(DPs0, DPs, _) -> get_datapoints(_Name, _Type, {_, _, once, _, match, Pat}) -> pattern_datapoints(Pat); get_datapoints(_Name, _Type, T) when is_tuple(T), is_list( - element(size(T),T)) -> + element(size(T),T)) -> element(size(T), T); get_datapoints(_Name, _Type, _Ref) -> [value]. @@ -580,20 +580,20 @@ e({element,E,T}, Bs) -> end; e({histogram, Vs}, Bs) -> case e(Vs, Bs) of - L when is_list(L) -> - exometer_util:histogram(L); - _ -> error(badarg) + L when is_list(L) -> + exometer_util:histogram(L); + _ -> error(badarg) end; e({histogram, Vs, DPs}, Bs) -> case e(Vs, Bs) of - L when is_list(L) -> - DataPoints = case e(DPs, Bs) of - default -> default; - D when is_list(D) -> D; - _ -> error(badarg) - end, - exometer_util:histogram(L, DataPoints); - _ -> error(badarg) + L when is_list(L) -> + DataPoints = case e(DPs, Bs) of + default -> default; + D when is_list(D) -> D; + _ -> error(badarg) + end, + exometer_util:histogram(L, DataPoints); + _ -> error(badarg) end; e({fold,Vx,Va,Es,Ea,El}, Bs) when is_atom(Vx), is_atom(Va) -> case e(El, Bs) of diff --git a/src/exometer_global.erl b/src/exometer_global.erl new file mode 100644 index 0000000..e30c1a4 --- /dev/null +++ b/src/exometer_global.erl @@ -0,0 +1,17 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at http://mozilla.org/MPL/2.0/. +%% +%% ------------------------------------------------------------------- +%% @private +-module(exometer_global). + +-export([status/0]). + +-spec status() -> enabled | disabled. +status() -> + enabled. diff --git a/src/exometer_histogram.erl b/src/exometer_histogram.erl index e4fdbd8..a6116b6 100644 --- a/src/exometer_histogram.erl +++ b/src/exometer_histogram.erl @@ -60,25 +60,25 @@ %% exometer_probe callbacks -export([behaviour/0, - probe_init/3, - probe_terminate/1, - probe_setopts/3, - probe_update/2, - probe_get_value/2, - probe_get_datapoints/1, - probe_reset/1, - probe_code_change/3, - probe_sample/1, - probe_handle_msg/2]). + probe_init/3, + probe_terminate/1, + probe_setopts/3, + probe_update/2, + probe_get_value/2, + probe_get_datapoints/1, + probe_reset/1, + probe_code_change/3, + probe_sample/1, + probe_handle_msg/2]). -compile(inline). -compile(inline_list_funcs). -export([datapoints/0]). -export([average_sample/3, - average_transform/2]). + average_transform/2]). -export([test_run/1, test_run/2, - test_series/0]). + test_series/0]). %% -compile({parse_transform, exometer_igor}). %% -compile({igor, [{files, ["src/exometer_util.erl" @@ -95,7 +95,7 @@ time_span = 60000, %% msec truncate = true, histogram_module = exometer_slot_slide, - heap, + heap, opts = []}). %% for auto-conversion @@ -109,30 +109,30 @@ behaviour() -> probe. probe_init(Name, _Type, Options) -> - {ok, init_state(Name, Options)}. + {ok, init_state(Name, Options)}. init_state(Name, Options) -> St = process_opts(#st{name = Name}, - [{histogram_module, exometer_slot_slide}, - {time_span, 60000}, - {slot_period, 10}] ++ Options), + [{histogram_module, exometer_slot_slide}, + {time_span, 60000}, + {slot_period, 10}] ++ Options), Slide = (St#st.histogram_module):new(St#st.time_span, - St#st.slot_period, - fun average_sample/3, - fun average_transform/2, - Options), + St#st.slot_period, + fun average_sample/3, + fun average_transform/2, + Options), Heap = if St#st.histogram_module == exometer_slot_slide -> - case lists:keyfind(keep_high, 1, Options) of - false -> - undefined; - {_, N} when is_integer(N), N > 0 -> - T = exometer_shallowtree:new(N), - {T, T}; - {_, 0} -> - undefined - end; - true -> undefined - end, + case lists:keyfind(keep_high, 1, Options) of + false -> + undefined; + {_, N} when is_integer(N), N > 0 -> + T = exometer_shallowtree:new(N), + {T, T}; + {_, 0} -> + undefined + end; + true -> undefined + end, St#st{slide = Slide, heap = Heap}. @@ -164,26 +164,26 @@ get_value_int(St, DataPoints) -> get_value_int_(#st{truncate = Trunc, histogram_module = Module, - time_span = TimeSpan, - heap = Heap} = St, DataPoints) -> + time_span = TimeSpan, + heap = Heap} = St, DataPoints) -> %% We need element count and sum of all elements to get mean value. Tot0 = case Trunc of true -> 0; round -> 0; false -> 0.0 end, TS = exometer_util:timestamp(), {Length, FullLength, Total, Min0, Max, Lst0, Xtra} = Module:foldl( - TS, + TS, fun ({_TS1, {Val, Cnt, NMin, NMax, X}}, {Length, FullLen, Total, OMin, OMax, List, Xs}) -> - {Length + 1, FullLen + Cnt, Total + Val, - min(OMin, NMin), max(OMax, NMax), - [Val|List], [X|Xs]}; + {Length + 1, FullLen + Cnt, Total + Val, + min(OMin, NMin), max(OMax, NMax), + [Val|List], [X|Xs]}; ({_TS1, Val}, {Length, _, Total, Min, Max, List, Xs}) -> - L1 = Length+1, - {L1, L1, Total + Val, min(Val, Min), max(Val, Max), - [Val|List], Xs} - end, + L1 = Length+1, + {L1, L1, Total + Val, min(Val, Min), max(Val, Max), + [Val|List], Xs} + end, {0, 0, Tot0, infinity, 0, [], []}, St#st.slide), Min = if Min0 == infinity -> 0; true -> Min0 end, Mean = case Length of @@ -205,23 +205,23 @@ get_value_int_(#st{truncate = Trunc, get_from_heap({New,Old}, TS, TSpan, N, DPs) when N > 0 -> Sz = exometer_shallowtree:size(New) - + exometer_shallowtree:size(Old), + + exometer_shallowtree:size(Old), if Sz > 0 -> - MinPerc = 100 - ((Sz*100) div N), - MinPerc10 = MinPerc * 10, - GetDPs = lists:foldl( - fun(D, Acc) when is_integer(D), - D < 100, D >= MinPerc -> - [{D, p(D, N)}|Acc]; - (D, Acc) when is_integer(D), - D > 100, D >= MinPerc10 -> - [{D, p(D, N)}|Acc]; - (_, Acc) -> - Acc - end, [], DPs), - pick_heap_vals(GetDPs, New, Old, TS, TSpan); + MinPerc = 100 - ((Sz*100) div N), + MinPerc10 = MinPerc * 10, + GetDPs = lists:foldl( + fun(D, Acc) when is_integer(D), + D < 100, D >= MinPerc -> + [{D, p(D, N)}|Acc]; + (D, Acc) when is_integer(D), + D > 100, D >= MinPerc10 -> + [{D, p(D, N)}|Acc]; + (_, Acc) -> + Acc + end, [], DPs), + pick_heap_vals(GetDPs, New, Old, TS, TSpan); true -> - [] + [] end; get_from_heap(_, _, _, _, _) -> []. @@ -232,13 +232,13 @@ pick_heap_vals(DPs, New, Old, TS, TSpan) -> TS0 = TS - TSpan, NewVals = exometer_shallowtree:filter(fun(V,_) -> {true,V} end, New), OldVals = exometer_shallowtree:filter( - fun(V,T) -> - if T >= TS0 -> - {true, V}; - true -> - false - end - end, Old), + fun(V,T) -> + if T >= TS0 -> + {true, V}; + true -> + false + end + end, Old), Vals = revsort(OldVals ++ NewVals), exometer_util:pick_items(Vals, DPs). @@ -246,18 +246,13 @@ revsort(L) -> lists:sort(fun erlang:'>'/2, L). p(50, N) -> perc(0.5, N); -p(75, N) -> perc(0.25, N); -p(90, N) -> perc(0.1, N); -p(95, N) -> perc(0.05, N); -p(99, N) -> perc(0.01, N); -p(999,N) -> perc(0.001, N). - -perc(P, Len) when P > 1.0 -> - round((P / 10) * Len) + 1; -perc(P, Len) -> - round(P * Len) + 1. - +p(75, N) -> perc(0.75, N); +p(90, N) -> perc(0.9, N); +p(95, N) -> perc(0.95, N); +p(99, N) -> perc(0.99, N); +p(999,N) -> perc(0.999, N). +perc(P, N) -> N - exometer_util:perc(P, N) + 1. add_extra(Length, L, []) -> {Length, L}; @@ -300,11 +295,16 @@ probe_setopts(_Entry, _Opts, _St) -> probe_update(Value, ?OLDSTATE = St) -> probe_update(Value, convert(St)); probe_update(Value, St) -> - {ok, update_int(exometer_util:timestamp(), Value, St)}. + if is_number(Value) -> + {ok, update_int(exometer_util:timestamp(), Value, St)}; + true -> + %% ignore + {ok, St} + end. update_int(Timestamp, Value, #st{slide = Slide, - histogram_module = Module, - heap = Heap} = St) -> + histogram_module = Module, + heap = Heap} = St) -> {Wrapped, Slide1} = Module:add_element(Timestamp, Value, Slide, true), St#st{slide = Slide1, heap = into_heap(Wrapped, Value, Timestamp, Heap)}. @@ -320,7 +320,7 @@ into_heap(true, Val, TS, {New,_}) -> probe_reset(?OLDSTATE = St) -> probe_reset(convert(St)); probe_reset(#st{slide = Slide, - histogram_module = Module} = St) -> + histogram_module = Module} = St) -> {ok, St#st{slide = Module:reset(Slide)}}. probe_sample(_St) -> @@ -335,10 +335,10 @@ probe_code_change(_, S, _) -> {ok, S}. convert({st, Name, Slide, Slot_period, Time_span, - Truncate, Histogram_module, Opts}) -> + Truncate, Histogram_module, Opts}) -> #st{name = Name, slide = Slide, slot_period = Slot_period, - time_span = Time_span, truncate = Truncate, - histogram_module = Histogram_module, opts = Opts}. + time_span = Time_span, truncate = Truncate, + histogram_module = Histogram_module, opts = Opts}. process_opts(St, Options) -> exometer_proc:process_options(Options), @@ -349,13 +349,13 @@ process_opts(St, Options) -> ( {slot_period, Val}, St1) -> St1#st {slot_period = Val}; ( {histogram_module, Val}, St1) -> St1#st {histogram_module = Val}; ( {truncate, Val}, St1) when is_boolean(Val); Val == round -> - St1#st{truncate = Val}; + St1#st{truncate = Val}; %% Unknown option, pass on to State options list, replacing %% any earlier versions of the same option. ({Opt, Val}, St1) -> - St1#st{ opts = [ {Opt, Val} - | lists:keydelete(Opt, 1, St1#st.opts) ] } - end, St, Options). + St1#st{ opts = [ {Opt, Val} + | lists:keydelete(Opt, 1, St1#st.opts) ] } + end, St, Options). -record(sample, {count, total, min, max, extra = []}). %% Simple sample processor that maintains an average @@ -436,13 +436,13 @@ test_run(Module, Interval) -> test_run(Module, Int, Series) -> St = test_new(test_opts(Module)), {T1, St1} = tc(fun() -> - test_update( - Series, Int, - exometer_util:timestamp(), St) - end), + test_update( + Series, Int, + exometer_util:timestamp(), St) + end), {T2, Result} = tc(fun() -> - get_value_int(St1, default) - end), + get_value_int(St1, default) + end), erlang:garbage_collect(), erlang:yield(), {T1, T2, Result}. @@ -494,14 +494,14 @@ tc(F) -> %% @end test_series() -> S = lists:flatten( - [dupl(200,3), - dupl(100,4), - dupl(200,5), - dupl(100,6), - dupl(200,7), - dupl(100,8), - dupl(80,9), - dupl(15,50), 80,81,82,83,100]), + [dupl(200,3), + dupl(100,4), + dupl(200,5), + dupl(100,6), + dupl(200,7), + dupl(100,8), + dupl(80,9), + dupl(15,50), 80,81,82,83,100]), shuffle(S ++ S ++ S ++ S ++ S ++ S ++ S ++ S ++ S). dupl(N,V) -> diff --git a/src/exometer_igor.erl b/src/exometer_igor.erl index f4668b4..3fd4100 100644 --- a/src/exometer_igor.erl +++ b/src/exometer_igor.erl @@ -8,8 +8,8 @@ parse_transform(Forms, Opts) -> Includes = [I || {i,I} <- Opts], NewForms = igor:parse_transform( - Forms, [{igor, IgorOpts ++ [{includes, Includes}, - {preprocess, true}]}|Opts]), + Forms, [{igor, IgorOpts ++ [{includes, Includes}, + {preprocess, true}]}|Opts]), fix_for_r16b03(NewForms). %% erl_syntax:revert/1 is horribly broken in R16B03. This transform diff --git a/src/exometer_info.erl b/src/exometer_info.erl index c5ef43c..f6d2235 100644 --- a/src/exometer_info.erl +++ b/src/exometer_info.erl @@ -20,10 +20,10 @@ -module(exometer_info). -export([status/1, - pp/1, - pp_lookup/1, - pp_find/1, - pp_select/1]). + pp/1, + pp_lookup/1, + pp_find/1, + pp_select/1]). -include("exometer.hrl"). -include_lib("parse_trans/include/exprecs.hrl"). @@ -74,10 +74,10 @@ pp(X) -> %% @end pp_lookup(Name) -> case exometer:info(Name, entry) of - undefined -> - undefined; - Entry -> - pp(Entry) + undefined -> + undefined; + Entry -> + pp(Entry) end. -spec pp_find(list()) -> [pp()]. diff --git a/src/exometer_probe.erl b/src/exometer_probe.erl index eee16da..c846aff 100644 --- a/src/exometer_probe.erl +++ b/src/exometer_probe.erl @@ -376,12 +376,36 @@ %% NewState}', where `NewState' contains the new state of the probe %% that reflects the processed message. %% - +%% == Fault tolerance == +%% Probes are supervised by the `exometer_admin' process, and can be restarted +%% after a crash. Restart parameters are provided via the option +%% `{restart, Params}', where `Params' is a list of `{Frequency, Action}' +%% tuples. `Frequency' is either `{Count, MilliSecs}' or ``'_''', and +%% the corresponding `Action :: restart | disable | delete' will be performed +%% if the frequency of restarts falls within the given limit. +%% +%% For example, `[{{3, 1000}, restart}]' will allow 3 restarts within a 1-second +%% window. The matching is performed from top to bottom, and the first matching +%% pattern is acted upon. If no matching pattern is found, the default action +%% is `delete'. A pattern ``{'_', Action}'' acts as a catch-all, and should +%% be put last in the list. +%% +%% It is also possible to specify ``{{Count, '_'}, Action}'', which means +%% that a total of `Count' restarts is permitted, regardless of how far apart +%% they are. The count is reset after each restart. +%% +%% Example: +%%
+%%   {restart, [{{3,1000}, restart},   % up to 3 restarts within 1 sec
+%%              {{4,'_'} , disable},   % a total of up to 4 restarts
+%%              {'_'     , delete}]}   % anything else
+%% 
+%% @end -module(exometer_probe). -behaviour(exometer_entry). -% exometer_entry callb +%% exometer_entry callbacks -export( [ behaviour/0, @@ -395,8 +419,17 @@ setopts/3 ]). +-export([start_probe/1, + stop_probe/1]). + +-export([restart/4]). + -include("exometer.hrl"). +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +-endif. + -record(st, { name, type, @@ -413,13 +446,13 @@ -type mod_state() :: any(). -type data_points() :: [atom()]. -type probe_reply() :: ok - | {ok, mod_state()} - | {ok, any(), mod_state()} - | {noreply, mod_state()} - | {error, any()}. + | {ok, mod_state()} + | {ok, any(), mod_state()} + | {noreply, mod_state()} + | {error, any()}. -type probe_noreply() :: ok - | {ok, mod_state()} - | {error, any()}. + | {ok, mod_state()} + | {error, any()}. -callback behaviour() -> exometer:behaviour(). -callback probe_init(name(), type(), options()) -> probe_noreply(). @@ -437,26 +470,124 @@ -callback probe_code_change(any(), mod_state(), any()) -> {ok, mod_state()}. new(Name, Type, [{arg, Module}|Opts]) -> + Restart = proplists:get_value(restart, Opts, default_restart()), + SpawnOpts = proplists:get_value(spawn_opts, Opts, spawn_opts()), { ok, exometer_proc:spawn_process( - Name, fun() -> - init(Name, Type, Module, Opts) - end) + Name, fun() -> + init(Name, Type, Module, Opts) + end, proc_opts(Name, Module, Restart, SpawnOpts)) }; - new(Name, Type, Options) -> %% Extract the module to use. {value, { module, Module }, Opts1 } = lists:keytake(module, 1, Options), new(Name, Type, [{arg, Module} | Opts1]). +proc_opts(Name, Module, Restart, SpawnOpts) when is_list(Restart) -> + proc_opts_(Name, Module, on_error_init(Restart), SpawnOpts); +proc_opts(Name, Module, Restart, SpawnOpts) -> + proc_opts_(Name, Module, Restart, SpawnOpts). + +proc_opts_(Name, Module, Restart, SpawnOpts) -> + OnError = on_error(Name, Module, Restart, SpawnOpts), + [{on_error, OnError}, {spawn_opts, SpawnOpts}]. + + +spawn_opts() -> + [{fullsweep_after, 10}]. + +on_error_init(R) when is_list(R) -> + {0, [], max_time(R), R}. + +on_error(Name, Module, R, SpawnOpts) -> + {restart, {?MODULE, restart, [Name, Module, R, SpawnOpts]}}. + +check_restart(Restart) -> + TS = exometer_util:timestamp(), + check_restart(TS, Restart). + +check_restart(TS, {Total, Hist, MaxT, R}) when is_integer(MaxT) -> + NewTotal = Total + 1, + Oldest = TS - MaxT, + NewHist = [TS | [T || T <- Hist, + T > Oldest]], + {action(NewTotal, NewHist, MaxT, R), {NewTotal, NewHist, MaxT, R}}; +check_restart(_TS, {Total, Hist, '_', R}) -> + NewTotal = Total + 1, + {action(NewTotal, [], '_', R), {NewTotal, Hist, '_', R}}. + + +max_time([{{_,T},_}|Rest]) -> + max_time(Rest, T); +max_time([{_,_}|Rest]) -> + max_time(Rest); +max_time([]) -> + '_'. + +max_time([{{_,T},_}|Rest], Max) when is_integer(T) -> + max_time(Rest, erlang:max(T, Max)); +max_time([_|Rest], Max) -> + max_time(Rest, Max); +max_time([], Max) -> + Max. + +action(_Total, _Hist, _MaxT, [{'_', Action}|_]) -> + Action; +action(Total, Hist, MaxT, [{{N,'_'}, Action}|R]) -> + if N >= Total -> Action; + true -> action(Total, Hist, MaxT, R) + end; +action(Total, Hist, MaxT, [{{N,T}, Action}|R]) -> + case within(Hist, N, T) of + true -> Action; + false -> + action(Total, Hist, MaxT, R) + end; +action(_, _, _, []) -> + delete. + +within([T|Ts], N, Tm) -> + within(Ts, 1, N, T - Tm); +within([], _, _) -> + false. + +within([H|T], N, M, Lim) when H > Lim -> + N1 = N + 1, + if N1 =< M -> + within(T, N1, M, Lim); + true -> + false + end; +within(_, _, _, _) -> + true. + +restart(Name, Module, Error, SpawnOpts) -> + Type = exometer:info(Name, type), + Opts = exometer:info(Name, options), + case check_restart(Error) of + {restart, Error1} -> + {ok, exometer_proc:spawn_process( + Name, fun() -> + init(Name, Type, Module, Opts) + end, + proc_opts(Name, Module, Error1, SpawnOpts))}; + {Other, _} when Other==delete; Other==disable -> + Other + end. + +default_restart() -> + [{{3, 60000}, restart}, + {'_', disable}]. + %% Should never be called directly for exometer_probe. behaviour() -> entry. +delete(_Name, _Type, undefined) -> + ok; delete(_Name, _Type, Pid) when is_pid(Pid) -> exometer_proc:cast(Pid, delete). - get_value(_Name, _Type, Pid) when is_pid(Pid) -> exometer_proc:call(Pid, {get_value, default}). @@ -478,6 +609,37 @@ reset(_Name, _Type, Pid) when is_pid(Pid) -> sample(_Name, _Type, Pid) when is_pid(Pid) -> exometer_proc:cast(Pid, sample). + +%% === housekeeping functions used e.g. at enable/disable + +%% @private +stop_probe(#exometer_entry{name = Name, + type = Type, + ref = Ref}) -> + exometer_cache:delete_name(Name), + if is_pid(Ref) -> + try exometer_probe:delete(Name, Type, Ref) + catch + error:_ -> + kill_probe(Ref) + end; + true -> + ok + end. + +kill_probe(Ref) when is_pid(Ref) -> + exometer_admin:demonitor(Ref), + exit(Ref, kill). + +%% @private +start_probe(#exometer_entry{module = Module, + name = Name, + type = Type, + options = Opts}) -> + new(Name, Type, [{ arg, Module} | Opts ]). + +%% == Probe implementation + init(Name, Type, Mod, Opts) -> process_flag(min_heap_size, 40000), {St0, Opts1} = process_opts(Opts, #st{name = Name, @@ -487,14 +649,14 @@ init(Name, Type, Mod, Opts) -> %% Create a new state for the module case {Mod:probe_init(Name, Type, St#st.opts), - St#st.sample_interval} of + St#st.sample_interval} of { ok, infinity} -> %% No sample timer to start. Return with undefined mod state - loop(St#st{ mod_state = undefined }); + loop(St#st{ mod_state = undefined }); {{ok, ModSt}, infinity} -> %% No sample timer to start. Return with the mod state returned by probe_init. - loop(St#st{ mod_state = ModSt }); + loop(St#st{ mod_state = ModSt }); {ok, _} -> %% Fire up the timer, with undefined mod @@ -505,7 +667,7 @@ init(Name, Type, Mod, Opts) -> loop(sample(St#st{ mod_state = ModSt })); {{error, Reason}, _} -> - %% FIXME: Proper shutdown. + %% FIXME: Proper shutdown. {error, Reason} end. @@ -518,21 +680,21 @@ loop(St) -> handle_msg(Msg, St) -> Module = St#st.module, case Msg of - {system, From, Req} -> - exometer_proc:handle_system_msg( - Req, From, St, fun(St1) -> loop(St1) end); + {system, From, Req} -> + exometer_proc:handle_system_msg( + Req, From, St, fun(St1) -> loop(St1) end); {exometer_proc, {From, Ref}, {get_value, default} } -> {ok, DataPoints} = Module:probe_get_datapoints(St#st.mod_state), {Reply, NSt} = - process_probe_reply(St, Module:probe_get_value(DataPoints, - St#st.mod_state)), + process_probe_reply(St, Module:probe_get_value(DataPoints, + St#st.mod_state)), From ! {Ref, Reply }, NSt; {exometer_proc, {From, Ref}, {get_value, DataPoints} } -> {Reply, NSt} = - process_probe_reply(St, Module:probe_get_value(DataPoints, - St#st.mod_state)), + process_probe_reply(St, Module:probe_get_value(DataPoints, + St#st.mod_state)), From ! {Ref, Reply }, NSt; @@ -556,14 +718,14 @@ handle_msg(Msg, St) -> {NSt, Opts1} = process_opts(Options, St), {Reply, NSt1} = %% Call module setopts for remainder of opts - process_probe_reply( - NSt, Module:probe_setopts(Entry, Opts1, NSt#st.mod_state)), + process_probe_reply( + NSt, Module:probe_setopts(Entry, Opts1, NSt#st.mod_state)), From ! {Ref, Reply }, %% Return state with options and any non-duplicate original opts. NSt1#st { opts = Opts1 ++ - [{K,V} || {K,V} <- St#st.opts, not lists:keymember(K,1,Opts1) ] + [{K,V} || {K,V} <- St#st.opts, not lists:keymember(K,1,Opts1) ] }; {timeout, _TRef, {exometer_proc, sample_timer}} -> @@ -632,3 +794,32 @@ process_opts([Opt|T], St, Acc) -> process_opts(T, St, [Opt | Acc]); process_opts([], St, Acc) -> {St, lists:reverse(Acc)}. + +%% EUnit + +-ifdef(TEST). + +restart_test_() -> + [ + ?_test(t_restart_1()), + ?_test(t_restart_2()) + ]. + +t_restart_1() -> + R = [{{3,1000}, restart}, % up to 3 restarts within 1 sec + {{4,'_'} , disable}, % a total of up to 4 restarts + {'_', delete}], % anything else + OE = on_error_init(R), + {restart, OE1} = check_restart(100, OE), + {restart, OE2} = check_restart(200, OE1), + {restart, OE3} = check_restart(300, OE2), + {disable, OE4} = check_restart(400, OE3), + {delete , _} = check_restart(500, OE4), + ok. + +t_restart_2() -> + R = [], + {delete, _} = check_restart({1, [500], 1000, R}), + ok. + +-endif. diff --git a/src/exometer_proc.erl b/src/exometer_proc.erl index 7e75107..7aa3248 100644 --- a/src/exometer_proc.erl +++ b/src/exometer_proc.erl @@ -38,6 +38,7 @@ -module(exometer_proc). -export([spawn_process/2, + spawn_process/3, cast/2, call/2, process_options/1, @@ -74,9 +75,19 @@ spawn_process(Name, F) when is_function(F,0) -> Parent = self(), proc_lib:spawn(fun() -> exometer_admin:monitor(Name, self()), - init(Name, Mod, F, Parent) + init(Name, Mod, F, Parent) end). +spawn_process(Name, F, Opts) -> + {_, Mod} = erlang:fun_info(F, module), + Parent = self(), + SpawnOpts = proplists:get_value(spawn_opts, Opts, []), + OnError = proplists:get_value(on_error, Opts, delete), + proc_lib:spawn_opt(fun() -> + exometer_admin:monitor(Name, self(), OnError), + init(Name, Mod, F, Parent) + end, SpawnOpts). + init(Name, Mod, StartF, ParentPid) -> I = #info{parent = ParentPid}, Sys = I#info.sys, @@ -201,8 +212,8 @@ format_status(Opt, StatusData) -> pid_to_list(Name); is_atom(Name) -> Name; - true -> - lists:flatten(io_lib:fwrite("~w", [Name])) + true -> + lists:flatten(io_lib:fwrite("~w", [Name])) end, Header = lists:concat(["Status for exometer_proc ", NameTag]), Log = sys:get_debug(log, Debug, []), diff --git a/src/exometer_report.erl b/src/exometer_report.erl index d154bd2..9e5dca3 100644 --- a/src/exometer_report.erl +++ b/src/exometer_report.erl @@ -160,6 +160,7 @@ set_interval/3, delete_interval/2, restart_intervals/1, + trigger_interval/2, get_intervals/1, remove_reporter/1, remove_reporter/2, terminate_reporter/1, @@ -189,8 +190,8 @@ -type error() :: {error, any()}. -type metric() :: exometer:name() - | {find, exometer:name()} - | {select, ets:match_spec()}. + | {find, exometer:name()} + | {select, ets:match_spec()}. -type datapoint() :: exometer:datapoint(). -type datapoints() :: datapoint() | [datapoint()]. -type options() :: [{atom(), any()}]. @@ -200,7 +201,7 @@ -type time_ms() :: pos_integer(). -type delay() :: time_ms(). -type named_interval() :: {atom(), time_ms()} - | {atom(), time_ms(), delay()}. + | {atom(), time_ms(), delay()}. -type callback_result() :: {ok, mod_state()} | any(). -type extra() :: any(). -type retry() :: boolean(). @@ -216,7 +217,7 @@ -callback exometer_init(options()) -> callback_result(). -callback exometer_report(metric(), datapoint(), - value(), extra(), mod_state()) -> + extra(), value(), mod_state()) -> callback_result(). -callback exometer_subscribe(metric(), datapoint(), @@ -240,7 +241,7 @@ any(). -callback exometer_setopts(exometer:entry(), options(), - exometer:status(), mod_state()) -> + exometer:status(), mod_state()) -> callback_result(). -callback exometer_newentry(exometer:entry(), mod_state()) -> @@ -267,11 +268,11 @@ ). -record(interval, { - name :: atom(), - time = 0 :: non_neg_integer(), - delay = 0 :: non_neg_integer(), - t_ref :: reference() | undefined - }). + name :: atom(), + time = 0 :: non_neg_integer() | 'manual', + delay = 0 :: non_neg_integer(), + t_ref :: reference() | undefined + }). -record(reporter, { name :: atom() | '_', @@ -279,9 +280,9 @@ mref :: reference() | '_', module :: module() | '_', opts = [] :: [{atom(), any()}] | '_', - intervals = [] :: [#interval{}] | '_', + intervals = [] :: [#interval{}] | '_', restart = #restart{} :: #restart{} | '_', - status = enabled :: enabled | disabled | '_' + status = enabled :: enabled | disabled | '_' }). -record(st, { @@ -302,13 +303,13 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec subscribe(reporter_name(), metric(), datapoints(), interval()) -> - ok | not_found | unknown_reporter | error. + ok | not_found | unknown_reporter | error. %% @equiv subscribe(Reporter, Metric, DataPoint, Interval, [], false) subscribe(Reporter, Metric, DataPoint, Interval) -> subscribe(Reporter, Metric, DataPoint, Interval, []). -spec subscribe(reporter_name(), metric(), datapoints(), interval(), extra()) -> - ok | not_found | unknown_reporter | error. + ok | not_found | unknown_reporter | error. %% @equiv subscribe(Reporter, Metric, DataPoint, Interval, Extra, false) subscribe(Reporter, Metric, DataPoint, Interval, Extra) -> call({subscribe, #key{reporter = Reporter, @@ -318,8 +319,8 @@ subscribe(Reporter, Metric, DataPoint, Interval, Extra) -> extra = Extra}, Interval}). -spec subscribe(reporter_name(), metric(), datapoints(), interval(), - extra(), retry()) -> - ok | not_found | unknown_reporter | error. + extra(), retry()) -> + ok | not_found | unknown_reporter | error. %% @doc Add a subscription to an existing reporter. %% %% The reporter must first be started using {@link add_reporter/2}, or through @@ -342,19 +343,19 @@ subscribe(Reporter, Metric, DataPoint, Interval, Extra) -> subscribe(Reporter, Metric, DataPoint, Interval, Extra, Retry) when is_boolean(Retry) -> call({subscribe, #key{reporter = Reporter, - metric = Metric, - datapoint = DataPoint, - retry_failed_metrics = Retry, - extra = Extra}, Interval}). + metric = Metric, + datapoint = DataPoint, + retry_failed_metrics = Retry, + extra = Extra}, Interval}). -spec unsubscribe(module(), metric(), datapoint()) -> - ok | not_found. + ok | not_found. %% @equiv unsubscribe(Reporter, Metric, DataPoint, []) unsubscribe(Reporter, Metric, DataPoint) -> unsubscribe(Reporter, Metric, DataPoint, []). -spec unsubscribe(module(), metric(), datapoint() | [datapoint()], extra()) -> - ok | not_found. + ok | not_found. %% @doc Removes a subscription. %% %% Note that the subscription is identified by the combination @@ -374,18 +375,18 @@ unsubscribe_all(Reporter, Metric) -> call({unsubscribe_all, Reporter, Metric}). -spec list_metrics() -> {ok, [{ exometer:name(), - [datapoint()], - [{reporter_name(), datapoint()}], - exometer:status() }]} | {error, any()}. + [datapoint()], + [{reporter_name(), datapoint()}], + exometer:status() }]} | {error, any()}. %% @equiv list_metrics([]) list_metrics() -> list_metrics([]). -spec list_metrics(Path :: metric()) -> - {ok, [{ exometer:name(), - [datapoint()], - [{reporter_name(), datapoint()}], - exometer:status() }]} | {error, any()}. + {ok, [{ exometer:name(), + [datapoint()], + [{reporter_name(), datapoint()}], + exometer:status() }]} | {error, any()}. %% @doc List all metrics matching `Path', together with subscription status. %% %% This function performs a metrics search using `exometer:find_entries/1', @@ -402,7 +403,7 @@ list_reporters() -> call(list_reporters). -spec list_subscriptions(reporter_name()) -> - [{metric(), datapoint(), interval(), extra()}]. + [{metric(), datapoint(), interval(), extra()}]. %% @doc List all subscriptions for `Reporter'. list_subscriptions(Reporter) -> call({list_subscriptions, Reporter}). @@ -426,12 +427,15 @@ list_subscriptions(Reporter) -> %% `{intervals, [named_interval()]}' %% named_interval() :: {Name::atom(), Interval::pos_integer()} %% | {Name::atom(), Interval::time_ms(), delay()::time_ms()} +%% | {Name::atom(), 'manual'} %% Define named intervals. The name can be used by subscribers, so that all %% subsriptions for a given named interval will be reported when the interval %% triggers. An optional delay (in ms) can be given: this will cause the first %% interval to start in `Delay' milliseconds. When all intervals are named %% at the same time, the delay parameter can be used to achieve staggered -%% reporting. +%% reporting. If the interval is specified as ```'manual'''', it will have +%% to be triggered manually using {@link trigger_interval/2}. +%% %% @end add_reporter(Reporter, Options) -> call({add_reporter, Reporter, Options}). @@ -442,24 +446,28 @@ remove_reporter(Reporter) -> call({remove_reporter, Reporter}). -spec set_interval(reporter_name(), atom(), - time_ms() | {time_ms(), delay()}) -> ok |error(). + time_ms() | {time_ms(), delay()}) -> ok |error(). %% @doc Specify a named interval. %% %% See {@link add_reporter/2} for a description of named intervals. %% The named interval is here specified as either `Time' (milliseconds) or -%% `{Time, Delay}', where a delay in milliseconds is provided. +%% `{Time, Delay}', where a delay in milliseconds is provided. It is also +%% specify an interval as ```'manual'''', indicating that the interval can +%% only be triggered manually via {@link trigger_interval/2}. %% %% If the named interval exists, it will be replaced with the new definition. %% Otherwise, it will be added. Use {@link restart_intervals/1} if you want %% all intervals to be restarted/resynched with corresponding relative delays. %% @end set_interval(Reporter, Name, Time) when is_atom(Name), - is_integer(Time), Time >= 0 -> + is_integer(Time), Time >= 0 -> call({set_interval, Reporter, Name, Time}); +set_interval(Reporter, Name, manual) when is_atom(Name) -> + call({set_interval, Reporter, Name, manual}); set_interval(Reporter, Name, {Time, Delay}) when is_atom(Name), - is_integer(Time), Time >= 0, - is_integer(Delay), - Delay >= 0 -> + is_integer(Time), Time >= 0, + is_integer(Delay), + Delay >= 0 -> call({set_interval, Reporter, Name, {Time, Delay}}). -spec delete_interval(reporter_name(), atom()) -> ok | error(). @@ -477,10 +485,21 @@ delete_interval(Reporter, Name) -> restart_intervals(Reporter) -> call({restart_intervals, Reporter}). +-spec trigger_interval(reporter_name(), atom()) -> ok. +%% @doc Trigger a named interval. +%% +%% This function is mainly used to trigger intervals defined as ```'manual'''', +%% but can be used to trigger any named interval. If a named interval with +%% a specified time in milliseconds is triggered this way, it will effectively +%% be restarted, and will repeat as usual from that point on. +%% @end +trigger_interval(Reporter, Name) -> + cast({trigger_interval, Reporter, Name}). + -spec get_intervals(reporter_name()) -> - [{atom(), [{time, pos_integer()} - | {delay, pos_integer()} - | {timer_ref, reference()}]}]. + [{atom(), [{time, pos_integer()} + | {delay, pos_integer()} + | {timer_ref, reference()}]}]. %% @doc List the named intervals for `Reporter'. get_intervals(Reporter) -> call({get_intervals, Reporter}). @@ -518,9 +537,9 @@ disable_reporter(Reporter) -> disable_me(Mod, St) -> cast({disable, self()}), receive - {exometer_terminate, shutdown} -> - Mod:exometer_terminate(shutdown, St), - exit(shutdown) + {exometer_terminate, shutdown} -> + Mod:exometer_terminate(shutdown, St), + exit(shutdown) end. -spec call_reporter(reporter_name(), any()) -> any() | {error, any()}. @@ -531,10 +550,10 @@ disable_me(Mod, St) -> %% @end call_reporter(Reporter, Msg) -> case ets:lookup(?EXOMETER_REPORTERS, Reporter) of - [#reporter{pid = Pid}] when is_pid(Pid) -> + [#reporter{pid = Pid}] when is_pid(Pid) -> exometer_proc:call(Pid, Msg); - [#reporter{status = disabled}] -> - {error, disabled}; + [#reporter{status = disabled}] -> + {error, disabled}; [] -> {error, {no_such_reporter, Reporter}} end. @@ -547,10 +566,10 @@ call_reporter(Reporter, Msg) -> %% @end cast_reporter(Reporter, Msg) -> case ets:lookup(?EXOMETER_REPORTERS, Reporter) of - [#reporter{pid = Pid}] when is_pid(Pid) -> + [#reporter{pid = Pid}] when is_pid(Pid) -> exometer_proc:cast(Pid, Msg); - [#reporter{status = disabled}] -> - {error, disabled}; + [#reporter{status = disabled}] -> + {error, disabled}; [] -> {error, {no_such_reporter, Reporter}} end. @@ -602,7 +621,7 @@ new_entry(Entry) -> %% @end %%-------------------------------------------------------------------- init([]) -> - process_flag(trap_exit, true), + process_flag(trap_exit, true), {ok, #st{}}. start_reporters() -> @@ -616,45 +635,45 @@ do_start_reporters(S) -> %% Traverse list of reporter and launch reporter gen servers as dynamic %% supervisor children. case lists:keyfind(reporters, 1, Opts) of - {reporters, ReporterList} -> - ReporterRecs = make_reporter_recs(ReporterList), - assert_no_duplicates(ReporterRecs), - lists:foreach( - fun(#reporter{name = Reporter, - status = Status, - opts = ROpts, - intervals = Ints0} = R) -> - Restart = get_restart(ROpts), - {Pid, MRef, Ints} = - if Status =:= enabled -> - {P1,R1} = spawn_reporter(Reporter, ROpts), - I1 = start_interval_timers(R), - {P1,R1,I1}; - true -> {undefined, undefined, Ints0} - end, - ets:insert(?EXOMETER_REPORTERS, - R#reporter{pid = Pid, - mref = MRef, - intervals = Ints, - restart = Restart}) - end, ReporterRecs); - false -> - [] + {reporters, ReporterList} -> + ReporterRecs = make_reporter_recs(ReporterList), + assert_no_duplicates(ReporterRecs), + lists:foreach( + fun(#reporter{name = Reporter, + status = Status, + opts = ROpts, + intervals = Ints0} = R) -> + Restart = get_restart(ROpts), + {Pid, MRef, Ints} = + if Status =:= enabled -> + {P1,R1} = spawn_reporter(Reporter, ROpts), + I1 = start_interval_timers(R), + {P1,R1,I1}; + true -> {undefined, undefined, Ints0} + end, + ets:insert(?EXOMETER_REPORTERS, + R#reporter{pid = Pid, + mref = MRef, + intervals = Ints, + restart = Restart}) + end, ReporterRecs); + false -> + [] end, %% Dig out configured 'static' subscribers case lists:keyfind(subscribers, 1, Opts) of - {subscribers, Subscribers} -> - lists:foreach(fun init_subscriber/1, Subscribers); - false -> [] + {subscribers, Subscribers} -> + lists:foreach(fun init_subscriber/1, Subscribers); + false -> [] end, S#st{}. make_reporter_recs([{R, Opts}|T]) when is_atom(R), is_list(Opts) -> [#reporter{name = R, module = get_module(R, Opts), - status = proplists:get_value(status, Opts, enabled), + status = proplists:get_value(status, Opts, enabled), opts = Opts, - intervals = get_interval_opts(Opts)}|make_reporter_recs(T)]; + intervals = get_interval_opts(Opts)}|make_reporter_recs(T)]; make_reporter_recs([]) -> []. @@ -667,41 +686,54 @@ get_interval_opts(Opts) -> Is = proplists:get_value(intervals, Opts, []), lists:map( fun({Name, Time}) when is_atom(Name), - is_integer(Time), Time >= 0 -> - #interval{name = Name, time = Time}; - ({Name, Time, Delay}) when is_atom(Name), - is_integer(Time), Time >= 0, - is_integer(Delay), Delay >= 0 -> - #interval{name = Name, time = Time, delay = Delay}; - (Other) -> - error({invalid_interval, Other}) + is_integer(Time), Time >= 0 -> + #interval{name = Name, time = Time}; + ({Name, Time, Delay}) when is_atom(Name), + is_integer(Time), Time >= 0, + is_integer(Delay), Delay >= 0 -> + #interval{name = Name, time = Time, delay = Delay}; + ({Name, manual}) when is_atom(Name) -> + #interval{name = Name, time = manual}; + (Other) -> + error({invalid_interval, Other}) end, Is ++ Is1). singelton_interval({N,T}=I) when is_atom(N), is_integer(T) -> I; singelton_interval({N,T,D}=I) when is_atom(N), - is_integer(T), - is_integer(D) -> I. + is_integer(T), + is_integer(D) -> I. start_interval_timers(#reporter{name = R, intervals = Ints}) -> lists:map(fun(I) -> start_interval_timer(I, R) end, Ints). +start_interval_timer(#interval{time = manual} = I, _) -> + I; start_interval_timer(#interval{name = Name, delay = Delay, - t_ref = Ref} = I, R) -> + t_ref = Ref} = I, R) -> cancel_timer(Ref), case Delay of - 0 -> - do_start_interval_timer(I, R); - D -> - TRef = erlang:send_after(D, self(), {start_interval, R, Name}), - I#interval{t_ref = TRef} + 0 -> + do_start_interval_timer(I, R); + D -> + TRef = erlang:send_after(D, self(), {start_interval, R, Name}), + I#interval{t_ref = TRef} end. do_start_interval_timer(#interval{name = Name, time = Time} = I, R) -> - TRef = erlang:send_after(Time, self(), batch_timer_msg(R, Name)), + TRef = erlang:send_after(Time, self(), batch_timer_msg(R, Name, Time)), I#interval{t_ref = TRef}. -batch_timer_msg(R, Name) -> - {report_batch, R, Name}. +batch_timer_msg(R, Name, Time) -> + batch_timer_msg(R, Name, Time, os:timestamp()). + +batch_timer_msg(R, Name, Time, TS) -> + {report_batch, R, Name, Time, TS}. + +subscr_timer_msg(Key, Interval) -> + subscr_timer_msg(Key, Interval, os:timestamp()). + +subscr_timer_msg(Key, Interval, TS) -> + {report, Key, Interval, TS}. get_report_env() -> Opts0 = exometer_util:get_env(report, []), @@ -756,29 +788,29 @@ handle_call({subscribe, %% Verify that the given metric/data point actually exist. case ets:lookup(?EXOMETER_REPORTERS, Reporter) of - [#reporter{status = Status}] -> - case is_valid_metric(Metric, DataPoint) of - true -> - if Status =:= enabled -> - Reporter ! {exometer_subscribe, Metric, - DataPoint, Interval, Extra}; - true -> ignore - end, + [#reporter{status = Status}] -> + case is_valid_metric(Metric, DataPoint) of + true -> + if Status =:= enabled -> + Reporter ! {exometer_subscribe, Metric, + DataPoint, Interval, Extra}; + true -> ignore + end, subscribe_(Reporter, Metric, DataPoint, - Interval, RetryFailedMetrics, - Extra, Status), + Interval, RetryFailedMetrics, + Extra, Status), {reply, ok, St}; %% Nope - Not found. - false -> - case RetryFailedMetrics of - true -> - subscribe_(Reporter, Metric, DataPoint, - Interval, RetryFailedMetrics, - Extra, Status), - {reply, ok, St}; - false -> - {reply, not_found, St} - end; + false -> + case RetryFailedMetrics of + true -> + subscribe_(Reporter, Metric, DataPoint, + Interval, RetryFailedMetrics, + Extra, Status), + {reply, ok, St}; + false -> + {reply, not_found, St} + end; error -> {reply, error, St} end; [] -> @@ -796,43 +828,43 @@ handle_call({unsubscribe, handle_call({unsubscribe_all, Reporter, Metric}, _, #st{}=St) -> Subs = ets:select(?EXOMETER_SUBS, - [{#subscriber{key = #key{reporter = Reporter, - metric = Metric, - _ = '_'}, - _ = '_'}, [], ['$_']}]), + [{#subscriber{key = #key{reporter = Reporter, + metric = Metric, + _ = '_'}, + _ = '_'}, [], ['$_']}]), lists:foreach(fun unsubscribe_/1, Subs), {reply, ok, St}; handle_call({list_metrics, Path}, _, St) -> if is_list(Path) -> - DP = lists:foldr(fun(Metric, Acc) -> - retrieve_metric(Metric, Acc) - end, [], exometer:find_entries(Path)), - {reply, {ok, DP}, St}; + DP = lists:foldr(fun(Metric, Acc) -> + retrieve_metric(Metric, Acc) + end, [], exometer:find_entries(Path)), + {reply, {ok, DP}, St}; true -> - {reply, {error, badarg}, St} + {reply, {error, badarg}, St} end; handle_call({list_subscriptions, Reporter}, _, #st{} = St) -> Subs1 = lists:foldl( fun (#subscriber{key=#key{reporter=Rep}}=Sub, Acc) when Reporter == Rep -> - #subscriber{ - key=#key{ - metric=Metric, - datapoint=Dp, - extra=Extra}, - interval=Interval} = Sub, - [{Metric, Dp, Interval, Extra} | Acc]; + #subscriber{ + key=#key{ + metric=Metric, + datapoint=Dp, + extra=Extra}, + interval=Interval} = Sub, + [{Metric, Dp, Interval, Extra} | Acc]; (_, Acc) -> - Acc - end, [], ets:select(?EXOMETER_SUBS, [{'_',[],['$_']}])), + Acc + end, [], ets:select(?EXOMETER_SUBS, [{'_',[],['$_']}])), {reply, Subs1, St}; handle_call(list_reporters, _, #st{} = St) -> Info = ets:select(?EXOMETER_REPORTERS, - [{#reporter{name = '$1', pid = '$2', _ = '_'}, - [], [{{'$1', '$2'}}]}]), + [{#reporter{name = '$1', pid = '$2', _ = '_'}, + [], [{{'$1', '$2'}}]}]), {reply, Info, St}; handle_call({add_reporter, Reporter, Opts}, _, #st{} = St) -> @@ -840,24 +872,24 @@ handle_call({add_reporter, Reporter, Opts}, _, #st{} = St) -> true -> {reply, {error, already_running}, St}; false -> - try - [R] = make_reporter_recs([{Reporter, Opts}]), - {Pid, MRef} = spawn_reporter(Reporter, Opts), - Ints = start_interval_timers(R), - R1 = R#reporter{intervals = Ints, - pid = Pid, - mref = MRef}, - ets:insert(?EXOMETER_REPORTERS, R1), - {reply, ok, St} - catch - error:Reason -> - {reply, {error, Reason}, St} - end + try + [R] = make_reporter_recs([{Reporter, Opts}]), + {Pid, MRef} = spawn_reporter(Reporter, Opts), + Ints = start_interval_timers(R), + R1 = R#reporter{intervals = Ints, + pid = Pid, + mref = MRef}, + ets:insert(?EXOMETER_REPORTERS, R1), + {reply, ok, St} + catch + error:Reason -> + {reply, {error, Reason}, St} + end end; handle_call({remove_reporter, Reporter}, _, St) -> case do_remove_reporter(Reporter) of - ok -> + ok -> {reply, ok, St}; E -> {reply, E, St} @@ -865,79 +897,82 @@ handle_call({remove_reporter, Reporter}, _, St) -> handle_call({change_reporter_status, Reporter, Status}, _, St) -> case change_reporter_status(Reporter, Status) of - ok -> - {reply, ok, St}; - E -> - {reply, E, St} + ok -> + {reply, ok, St}; + E -> + {reply, E, St} end; handle_call({set_interval, Reporter, Name, Int}, _, #st{}=St) -> case ets:lookup(?EXOMETER_REPORTERS, Reporter) of - [#reporter{intervals = Ints}] -> - try - I0 = case lists:keyfind(Name, #interval.name, Ints) of - false -> #interval{name = Name}; - Interval -> Interval - end, - I1 = case Int of - {Time, Delay} when is_integer(Time), Time >= 0, - is_integer(Delay), Delay >= 0 -> - I0#interval{time = Time, delay = Delay}; - Time when is_integer(Time), Time >= 0 -> - I0#interval{time = Time} - end, - ets:update_element(?EXOMETER_REPORTERS, Reporter, - [{#reporter.intervals, - lists:keystore( - Name, #interval.name, Ints, - start_interval_timer(I1, Reporter))}]), - {reply, ok, St} - catch - error:Reason -> - {reply, {error, Reason}, St} - end; - [] -> - {reply, {error, not_found}, St} + [#reporter{intervals = Ints}] -> + try + I0 = case lists:keyfind(Name, #interval.name, Ints) of + false -> #interval{name = Name}; + Interval -> Interval + end, + I1 = case Int of + {Time, Delay} when is_integer(Time), Time >= 0, + is_integer(Delay), Delay >= 0 -> + I0#interval{time = Time, delay = Delay}; + Time when is_integer(Time), Time >= 0 -> + I0#interval{time = Time}; + manual -> + cancel_timer(I0#interval.t_ref), + I0#interval{time = manual} + end, + ets:update_element(?EXOMETER_REPORTERS, Reporter, + [{#reporter.intervals, + lists:keystore( + Name, #interval.name, Ints, + start_interval_timer(I1, Reporter))}]), + {reply, ok, St} + catch + error:Reason -> + {reply, {error, Reason}, St} + end; + [] -> + {reply, {error, not_found}, St} end; handle_call({delete_interval, Reporter, Name}, _, #st{} = St) -> case ets:lookup(?EXOMETER_REPORTERS, Reporter) of - [#reporter{intervals = Ints}] -> - case lists:keyfind(Name, #interval.name, Ints) of - #interval{t_ref = TRef} -> - cancel_timer(TRef), - ets:update_element(?EXOMETER_REPORTERS, Reporter, - [{#reporter.intervals, - lists:keydelete( - Name, #interval.name, Ints)}]), - {reply, ok, St}; - false -> - {reply, {error, not_found}, St} - end; - [] -> - {reply, {error, not_found}, St} + [#reporter{intervals = Ints}] -> + case lists:keyfind(Name, #interval.name, Ints) of + #interval{t_ref = TRef} -> + cancel_timer(TRef), + ets:update_element(?EXOMETER_REPORTERS, Reporter, + [{#reporter.intervals, + lists:keydelete( + Name, #interval.name, Ints)}]), + {reply, ok, St}; + false -> + {reply, {error, not_found}, St} + end; + [] -> + {reply, {error, not_found}, St} end; handle_call({restart_intervals, Reporter}, _, #st{} = St) -> case ets:lookup(?EXOMETER_REPORTERS, Reporter) of - [#reporter{} = R] -> - Ints = start_interval_timers(R), - ets:update_element(?EXOMETER_REPORTERS, Reporter, - [{#reporter.intervals, Ints}]), - {reply, ok, St}; - [] -> - {reply, {error, not_found}, St} + [#reporter{} = R] -> + Ints = start_interval_timers(R), + ets:update_element(?EXOMETER_REPORTERS, Reporter, + [{#reporter.intervals, Ints}]), + {reply, ok, St}; + [] -> + {reply, {error, not_found}, St} end; handle_call({get_intervals, Reporter}, _, #st{} = St) -> case ets:lookup(?EXOMETER_REPORTERS, Reporter) of - [#reporter{intervals = Ints}] -> - Info = - [{Name, [{time, T}, - {delay, D}, - {timer_ref, TR}]} || #interval{name = Name, - time = T, - delay = D, - t_ref = TR} <- Ints], - {reply, Info, St}; - [] -> - {reply, {error, not_found}, St} + [#reporter{intervals = Ints}] -> + Info = + [{Name, [{time, T}, + {delay, D}, + {timer_ref, TR}]} || #interval{name = Name, + time = T, + delay = D, + t_ref = TR} <- Ints], + {reply, Info, St}; + [] -> + {reply, {error, not_found}, St} end; handle_call({setopts, Metric, Options, Status}, _, #st{}=St) -> [erlang:send(Pid, {exometer_setopts, Metric, Options, Status}) @@ -974,11 +1009,14 @@ handle_cast({remove_reporter, Reporter, Reason}, St) -> {noreply, St}; handle_cast({disable, Pid}, #st{} = St) -> case reporter_by_pid(Pid) of - [#reporter{} = Reporter] -> - do_change_reporter_status(Reporter, disabled); - [] -> ok + [#reporter{} = Reporter] -> + do_change_reporter_status(Reporter, disabled); + [] -> ok end, {noreply, St}; +handle_cast({trigger_interval, Reporter, Name}, #st{} = St) -> + report_batch(Reporter, Name, os:timestamp()), + {noreply, St}; handle_cast(_Msg, State) -> {noreply, State}. @@ -993,57 +1031,57 @@ handle_cast(_Msg, State) -> %%-------------------------------------------------------------------- handle_info({start_interval, Reporter, Name}, #st{} = St) -> case ets:lookup(?EXOMETER_REPORTERS, Reporter) of - [#reporter{intervals = Ints, status = enabled}] -> - case lists:keyfind(Name, #interval.name, Ints) of - #interval{} = I -> - I1 = do_start_interval_timer(I, Reporter), - ets:update_element(?EXOMETER_REPORTERS, Reporter, - [{#reporter.intervals, - lists:keyreplace( - Name, #interval.name, Ints, I1)}]); - false -> - ok - end; - _ -> - ok + [#reporter{intervals = Ints, status = enabled}] -> + case lists:keyfind(Name, #interval.name, Ints) of + #interval{time = Time} = I when is_integer(Time) -> + I1 = do_start_interval_timer(I, Reporter), + ets:update_element(?EXOMETER_REPORTERS, Reporter, + [{#reporter.intervals, + lists:keyreplace( + Name, #interval.name, Ints, I1)}]); + #interval{time = manual} -> + ok; + false -> + ok + end; + _ -> + ok end, {noreply, St}; handle_info({report_batch, Reporter, Name}, #st{} = St) -> %% Find all entries where reporter is Reporter and interval is Name, %% and report them. - report_batch(Reporter, Name), + report_batch(Reporter, Name, os:timestamp()), {noreply, St}; -handle_info({report, #key{reporter = Reporter} = Key, Interval}, #st{} = St) -> - case ets:member(?EXOMETER_SUBS, Key) andalso - get_reporter_status(Reporter) == enabled of - true -> - case do_report(Key, Interval) of - true -> restart_subscr_timer(Key, Interval); - false -> ok - end, - {noreply, St}; - false -> - %% Possibly an unsubscribe removed the subscriber - ?error("No such subscriber (Key=~p)~n", [Key]), - {noreply, St} - end; +handle_info({report_batch, Reporter, Name, Int, TS}, #st{} = St) -> + %% Find all entries where reporter is Reporter and interval is Name, + %% and report them. + TS1 = calc_fire_time(TS, Int), + report_batch(Reporter, Name, TS1), + {noreply, St}; +handle_info({report, #key{} = Key, Interval}, #st{} = St) -> + %% BW Compat. Old-style timeout msg, which doesn't include timestamp + {noreply, handle_report(Key, Interval, os:timestamp(), St)}; +handle_info({report, #key{} = Key, Interval, TS}, #st{} = St) -> + TS1 = calc_fire_time(TS, Interval), + {noreply, handle_report(Key, Interval, TS1, St)}; handle_info({'DOWN', Ref, process, _Pid, Reason}, #st{} = S) -> case reporter_by_mref(Ref) of - [#reporter{module = Module, restart = Restart} = R] -> - case add_restart(Restart) of - {remove, How} -> - case How of - {M, F} when is_atom(M), is_atom(F) -> - try M:F(Module, Reason) catch _:_ -> ok end; - _ -> - ok - end, - S; - {restart, Restart1} -> - restart_reporter(R#reporter{restart = Restart1}) - end; - _ -> S + [#reporter{module = Module, restart = Restart} = R] -> + case add_restart(Restart) of + {remove, How} -> + case How of + {M, F} when is_atom(M), is_atom(F) -> + try M:F(Module, Reason) catch _:_ -> ok end; + _ -> + ok + end, + S; + {restart, Restart1} -> + restart_reporter(R#reporter{restart = Restart1}) + end; + _ -> S end, {noreply, S}; @@ -1054,123 +1092,158 @@ handle_info(_Info, State) -> restart_reporter(#reporter{name = Name, opts = Opts, restart = Restart}) -> {Pid, MRef} = spawn_reporter(Name, Opts), [resubscribe(S) || - S <- ets:select(?EXOMETER_SUBS, - [{#subscriber{key = #key{reporter = Name, - _ = '_'}, - _ = '_'}, [], ['$_']}])], + S <- ets:select(?EXOMETER_SUBS, + [{#subscriber{key = #key{reporter = Name, + _ = '_'}, + _ = '_'}, [], ['$_']}])], ets:update_element(?EXOMETER_REPORTERS, Name, - [{#reporter.pid, Pid}, - {#reporter.mref, MRef}, - {#reporter.restart, Restart}, - {#reporter.status, enabled}]), + [{#reporter.pid, Pid}, + {#reporter.mref, MRef}, + {#reporter.restart, Restart}, + {#reporter.status, enabled}]), ok. %% If there are already subscriptions, enable them. maybe_enable_subscriptions(#exometer_entry{name = Metric}) -> lists:foreach( fun(#subscriber{key = #key{reporter = RName}} = S) -> - case get_reporter_status(RName) of - enabled -> - resubscribe(S); - _ -> - ok - end + case get_reporter_status(RName) of + enabled -> + resubscribe(S); + _ -> + ok + end end, ets:select(?EXOMETER_SUBS, - [{#subscriber{key = #key{metric = Metric, - _ = '_'}, - _ = '_'}, [], ['$_']}])). + [{#subscriber{key = #key{metric = Metric, + _ = '_'}, + _ = '_'}, [], ['$_']}])). resubscribe(#subscriber{key = #key{reporter = RName, - metric = Metric, - datapoint = DataPoint, - extra = Extra} = Key, - t_ref = OldTRef, - interval = Interval}) -> + metric = Metric, + datapoint = DataPoint, + extra = Extra} = Key, + t_ref = OldTRef, + interval = Interval}) when is_integer(Interval) -> try_send(RName, {exometer_subscribe, Metric, DataPoint, Interval, Extra}), cancel_timer(OldTRef), - TRef = erlang:send_after(Interval, self(), {report, Key, Interval}), - ets:update_element(?EXOMETER_SUBS, Key, [{#subscriber.t_ref, TRef}]). - + TRef = erlang:send_after(Interval, self(), + subscr_timer_msg(Key, Interval)), + ets:update_element(?EXOMETER_SUBS, Key, [{#subscriber.t_ref, TRef}]); + +resubscribe(_) -> undefined. + +handle_report(#key{reporter = Reporter} = Key, Interval, TS, #st{} = St) -> + _ = case ets:member(?EXOMETER_SUBS, Key) andalso + get_reporter_status(Reporter) == enabled of + true -> + case do_report(Key, Interval) of + true -> restart_subscr_timer(Key, Interval, TS); + false -> ok + end; + false -> + %% Possibly an unsubscribe removed the subscriber + ?error("No such subscriber (Key=~p)~n", [Key]) + end, + St. do_report(#key{metric = Metric, - datapoint = DataPoint, - retry_failed_metrics = RetryFailedMetrics} = Key, Interval) -> + datapoint = DataPoint, + retry_failed_metrics = RetryFailedMetrics} = Key, Interval) -> case {RetryFailedMetrics, get_values(Metric, DataPoint)} of - %% We found a value, or values. - {_, [_|_] = Found} -> - %% Distribute metric value to the correct process - report_values(Found, Key), - true; - %% We did not find a value, but we should try again. - {true, _ } -> - if is_list(Metric) -> - ?debug("Metric(~p) Datapoint(~p) not found." - " Will try again in ~p msec~n", - [Metric, DataPoint, Interval]), - true; - true -> false - end; - %% We did not find a value, and we should not retry. - _ -> - %% Entry removed while timer in progress. - ?warning("Metric(~p) Datapoint(~p) not found. Will not try again~n", - [Metric, DataPoint]), - false + %% We found a value, or values. + {_, [_|_] = Found} -> + %% Distribute metric value to the correct process + report_values(Found, Key), + true; + %% We did not find a value, but we should try again. + {true, _ } -> + if is_list(Metric) -> + ?debug("Metric(~p) Datapoint(~p) not found." + " Will try again in ~p msec~n", + [Metric, DataPoint, Interval]), + true; + true -> false + end; + %% We did not find a value, and we should not retry. + _ -> + %% Entry removed while timer in progress. + ?warning("Metric(~p) Datapoint(~p) not found. Will not try again~n", + [Metric, DataPoint]), + false end. -report_batch(Reporter, Name) when is_atom(Name) -> +report_batch(Reporter, Name, T0) when is_atom(Name) -> case ets:lookup(?EXOMETER_REPORTERS, Reporter) of - [#reporter{status = disabled}] -> - false; - [R] -> - Entries = ets:select(?EXOMETER_SUBS, - [{#subscriber{key = #key{reporter = Reporter, - _ = '_'}, - interval = Name, - _ = '_'}, [], ['$_']}]), - lists:foreach( - fun(#subscriber{key = Key}) -> - do_report(Key, Name) - end, Entries), - restart_batch_timer(Name, R); - [] -> - false + [#reporter{status = disabled}] -> + false; + [R] -> + Entries = ets:select(?EXOMETER_SUBS, + [{#subscriber{key = #key{reporter = Reporter, + _ = '_'}, + interval = Name, + _ = '_'}, [], ['$_']}]), + lists:foreach( + fun(#subscriber{key = Key}) -> + do_report(Key, Name) + end, Entries), + restart_batch_timer(Name, R, T0); + [] -> + false end. cancel_subscr_timers(Reporter) -> lists:foreach( fun(#subscriber{key = Key, t_ref = TRef}) -> - cancel_timer(TRef), - ets:update_element(?EXOMETER_SUBS, Key, - [{#subscriber.t_ref, undefined}]) + cancel_timer(TRef), + ets:update_element(?EXOMETER_SUBS, Key, + [{#subscriber.t_ref, undefined}]) end, ets:select(?EXOMETER_SUBS, - [{#subscriber{key = #key{reporter = Reporter, - _ = '_'}, - _ = '_'}, [], ['$_']}])). + [{#subscriber{key = #key{reporter = Reporter, + _ = '_'}, + _ = '_'}, [], ['$_']}])). -restart_subscr_timer(Key, Interval) when is_integer(Interval) -> - TRef = erlang:send_after(Interval, self(), - {report, Key, Interval}), +restart_subscr_timer(Key, Interval, T0) when is_integer(Interval) -> + TRef = erlang:send_after(adjust_interval(Interval, T0), self(), + subscr_timer_msg(Key, Interval, T0)), ets:update_element(?EXOMETER_SUBS, Key, - [{#subscriber.t_ref, TRef}]); -restart_subscr_timer(_, _) -> + [{#subscriber.t_ref, TRef}]); +restart_subscr_timer(_, _, _) -> true. restart_batch_timer(Name, #reporter{name = Reporter, - intervals = Ints}) when is_list(Ints) -> + intervals = Ints}, T0) when is_list(Ints) -> case lists:keyfind(Name, #interval.name, Ints) of - #interval{time = Time} = I -> - TRef = erlang:send_after(Time, self(), - batch_timer_msg(Reporter, Name)), - ets:update_element(?EXOMETER_REPORTERS, Reporter, - [{#reporter.intervals, - lists:keyreplace(Name, #interval.name, Ints, - I#interval{t_ref = TRef})}]); - false -> - false + #interval{time = Time, t_ref = OldTRef} = I when is_integer(Time) -> + cancel_timer(OldTRef), + TRef = erlang:send_after( + adjust_interval(Time, T0), self(), + batch_timer_msg(Reporter, Name, Time, T0)), + ets:update_element(?EXOMETER_REPORTERS, Reporter, + [{#reporter.intervals, + lists:keyreplace(Name, #interval.name, Ints, + I#interval{t_ref = TRef})}]); + #interval{time = manual} -> + false; + false -> + false end. +adjust_interval(Time, T0) -> + T1 = os:timestamp(), + erlang:max(0, Time - tdiff(T1, T0)). + +tdiff(T1, T0) -> + timer:now_diff(T1, T0) div 1000. + +%% Calculate time when timer should have fired, based on timestamp logged +%% at send_after/3 and the intended interval (in ms). +calc_fire_time({manual, TS}, _) -> + TS; +calc_fire_time({M,S,U}, Int) -> + {M, S, U + (Int*1000)}. + + cancel_timer(undefined) -> false; cancel_timer(TRef) -> @@ -1215,14 +1288,14 @@ code_change(_OldVan, #st{reporters = Rs, subscribers = Ss} = S, _Extra) -> #reporter{name = Module, pid = Pid, mref = MRef, module = Module, opts = Opts, restart = Restart}; - ({reporter,Name,Pid,MRef,Module,Opts,Restart}) -> - #reporter{name = Name, pid = Pid, mref = MRef, - module = Module, opts = Opts, - restart = Restart}; - ({reporter,Name,Pid,Mref,Module,Opts,Restart,Status}) -> - #reporter{name = Name, pid = Pid, mref = Mref, - module = Module, opts = Opts, - restart = Restart, status = Status}; + ({reporter,Name,Pid,MRef,Module,Opts,Restart}) -> + #reporter{name = Name, pid = Pid, mref = MRef, + module = Module, opts = Opts, + restart = Restart}; + ({reporter,Name,Pid,Mref,Module,Opts,Restart,Status}) -> + #reporter{name = Name, pid = Pid, mref = Mref, + module = Module, opts = Opts, + restart = Restart, status = Status}; (#reporter{} = R) -> R end, Rs), [ets:insert(?EXOMETER_REPORTERS, R) || R <- Rs1], @@ -1237,22 +1310,22 @@ code_change(_OldVsn, State, _Extra) -> reporter_pids() -> ets:select(?EXOMETER_REPORTERS, - [{#reporter{pid = '$1', _ = '_'}, - [{is_pid,'$1'}], ['$1']}]). + [{#reporter{pid = '$1', _ = '_'}, + [{is_pid,'$1'}], ['$1']}]). reporter_by_pid(Pid) -> ets:select(?EXOMETER_REPORTERS, - [{#reporter{pid = Pid, _='_'}, [], ['$_']}]). + [{#reporter{pid = Pid, _='_'}, [], ['$_']}]). reporter_by_mref(Ref) -> ets:select(?EXOMETER_REPORTERS, - [{#reporter{mref = Ref, _='_'}, [], ['$_']}]). + [{#reporter{mref = Ref, _='_'}, [], ['$_']}]). try_send(To, Msg) -> try To ! Msg catch - error:_ -> - Msg + error:_ -> + Msg end. is_valid_metric({find, Name}, _DataPoint) when is_list(Name) -> @@ -1260,25 +1333,25 @@ is_valid_metric({find, Name}, _DataPoint) when is_list(Name) -> is_valid_metric({select, Name}, _DataPoint) when is_list(Name) -> try ets:match_spec_compile(Name), true catch - error:_ -> error + error:_ -> error end; is_valid_metric(Name, default) when is_list(Name) -> case exometer:info(Name, type) of - undefined -> false; - _ -> true + undefined -> false; + _ -> true end; is_valid_metric(Name, DataPoint) when is_list(Name) -> case dp_list(DataPoint) of - [] -> error; - [_|_] = DataPoints -> - case exometer:info(Name, datapoints) of - undefined -> false; - DPs -> - case DataPoints -- DPs of - [] -> true; - _ -> false - end - end + [] -> error; + [_|_] = DataPoints -> + case exometer:info(Name, datapoints) of + undefined -> false; + DPs -> + case DataPoints -- DPs of + [] -> true; + _ -> false + end + end end; is_valid_metric(_, _) -> false. @@ -1294,25 +1367,25 @@ dp_list(999) -> [999]. get_values(Name, DataPoint) when is_list(Name) -> case exometer:get_value(Name, DataPoint) of - {ok, Values} when is_list(Values) -> - [{Name, Values}]; - _ -> - [] + {ok, Values} when is_list(Values) -> + [{Name, Values}]; + _ -> + [] end; get_values({How, Path}, DataPoint) -> Entries = case How of - find -> exometer:find_entries(Path); - select -> exometer:select(Path) - end, + find -> exometer:find_entries(Path); + select -> exometer:select(Path) + end, lists:foldr( fun({Name, _, enabled}, Acc) -> - case exometer:get_value(Name, DataPoint) of - {ok, Values} when is_list(Values) -> - [{Name, Values}|Acc]; - _ -> - Acc - end; - (_, Acc) -> Acc + case exometer:get_value(Name, DataPoint) of + {ok, Values} when is_list(Values) -> + [{Name, Values}|Acc]; + _ -> + Acc + end; + (_, Acc) -> Acc end, [], Entries). @@ -1329,7 +1402,7 @@ spawn_reporter(Reporter, Opt) when is_atom(Reporter), is_list(Opt) -> Fun = fun() -> maybe_register(Reporter, Opt), {ok, Mod, St} = reporter_init(Reporter, Opt), - reporter_loop(Mod, St) + reporter_loop(Mod, St) end, Pid = proc_lib:spawn(Fun), MRef = erlang:monitor(process, Pid), @@ -1357,20 +1430,21 @@ terminate_reporter(#reporter{pid = undefined}) -> subscribe_(Reporter, Metric, DataPoint, Interval, RetryFailedMetrics, - Extra, Status) -> + Extra, Status) -> Key = #key{reporter = Reporter, - metric = Metric, - datapoint = DataPoint, - extra = Extra, - retry_failed_metrics = RetryFailedMetrics - }, + metric = Metric, + datapoint = DataPoint, + extra = Extra, + retry_failed_metrics = RetryFailedMetrics + }, ets:insert(?EXOMETER_SUBS, - #subscriber{key = Key, - interval = Interval, - t_ref = maybe_send_after(Status, Key, Interval)}). + #subscriber{key = Key, + interval = Interval, + t_ref = maybe_send_after(Status, Key, Interval)}). maybe_send_after(enabled, Key, Interval) when is_integer(Interval) -> - erlang:send_after(Interval, self(), {report, Key, Interval}); + erlang:send_after( + Interval, self(), subscr_timer_msg(Key, Interval)); maybe_send_after(_, _, _) -> undefined. @@ -1378,19 +1452,19 @@ unsubscribe_(Reporter, Metric, DataPoint, Extra) -> ?info("unsubscribe_(~p, ~p, ~p, ~p)~n", [ Reporter, Metric, DataPoint, Extra]), case ets:lookup(?EXOMETER_SUBS, #key{reporter = Reporter, - metric = Metric, - datapoint = DataPoint, - extra = Extra}) of - [#subscriber{} = Sub] -> - unsubscribe_(Sub); + metric = Metric, + datapoint = DataPoint, + extra = Extra}) of + [#subscriber{} = Sub] -> + unsubscribe_(Sub); [] -> not_found end. unsubscribe_(#subscriber{key = #key{reporter = Reporter, - metric = Metric, - datapoint = DataPoint, - extra = Extra} = Key, t_ref = TRef}) -> + metric = Metric, + datapoint = DataPoint, + extra = Extra} = Key, t_ref = TRef}) -> try_send( Reporter, {exometer_unsubscribe, Metric, DataPoint, Extra}), cancel_timer(TRef), @@ -1400,12 +1474,12 @@ unsubscribe_(#subscriber{key = #key{reporter = Reporter, report_values(Found, #key{reporter = Reporter, extra = Extra} = Key) -> try - [[report_value(Reporter, Name, DP, Extra, Val) - || {DP, Val} <- Values] || {Name, Values} <- Found] + [[report_value(Reporter, Name, DP, Extra, Val) + || {DP, Val} <- Values] || {Name, Values} <- Found] catch - error:Reason -> - lager:error("ERROR ~p~nKey = ~p~nTrace: ~p", - [Reason, Key, erlang:get_stacktrace()]) + error:Reason -> + lager:error("ERROR ~p~nKey = ~p~nTrace: ~p", + [Reason, Key, erlang:get_stacktrace()]) end. report_value(Reporter, Metric, DataPoint, Extra, Val) -> @@ -1418,9 +1492,9 @@ report_value(Reporter, Metric, DataPoint, Extra, Val) -> retrieve_metric({Metric, Type, Enabled}, Acc) -> Cands = ets:select( - ?EXOMETER_SUBS, - [{#subscriber{key = #key{metric = Metric, _='_'}, - _ = '_'}, [], ['$_']}]), + ?EXOMETER_SUBS, + [{#subscriber{key = #key{metric = Metric, _='_'}, + _ = '_'}, [], ['$_']}]), [ { Metric, exometer:info(Metric, datapoints), get_subscribers(Metric, Type, Enabled, Cands), Enabled } | Acc ]. @@ -1437,12 +1511,12 @@ get_subscribers(_Metric, _Type, _Status, []) -> %% This subscription matches Metric get_subscribers(Metric, Type, Status, - [ #subscriber { - key = #key { - reporter = SReporter, - metric = Metric, - datapoint = SDataPoint - }} | T ]) -> + [ #subscriber { + key = #key { + reporter = SReporter, + metric = Metric, + datapoint = SDataPoint + }} | T ]) -> ?debug("get_subscribers(~p, ~p, ~p): match~n", [ Metric, SDataPoint, SReporter]), [ { SReporter, SDataPoint } | get_subscribers(Metric, Type, Status, T) ]; @@ -1463,14 +1537,14 @@ get_subscribers(Metric, Type, Status, %% This subscription does not match Metric. get_subscribers(Metric, Type, Status, - [ #subscriber { - key = #key { - reporter = SReporter, - metric = SMetric, - datapoint = SDataPoint - }} | T]) -> + [ #subscriber { + key = #key { + reporter = SReporter, + metric = SMetric, + datapoint = SDataPoint + }} | T]) -> ?debug("get_subscribers(~p, ~p, ~p) nomatch(~p) ~n", - [ SMetric, SDataPoint, SReporter, Metric]), + [ SMetric, SDataPoint, SReporter, Metric]), get_subscribers(Metric, Type, Status, T). %% Purge all subscriptions associated with a specific reporter @@ -1482,12 +1556,12 @@ purge_subscriptions(R) -> %% Return new #subscriber list with all original subscribers %% that do not reference reporter R. Subs = ets:select(?EXOMETER_SUBS, - [{#subscriber{key = #key{reporter = R, _='_'}, - _ = '_'}, [], ['$_']}]), + [{#subscriber{key = #key{reporter = R, _='_'}, + _ = '_'}, [], ['$_']}]), lists:foreach(fun(#subscriber {key = Key, t_ref = TRef}) -> - cancel_timer(TRef), - ets:delete(?EXOMETER_SUBS, Key) - end, Subs). + cancel_timer(TRef), + ets:delete(?EXOMETER_SUBS, Key) + end, Subs). %% Called by the spawn_monitor() call in init %% Loop and run reporters. @@ -1496,7 +1570,7 @@ reporter_init(Reporter, Opts) -> Module = proplists:get_value(module, Opts, Reporter), case Module:exometer_init(Opts) of {ok, St} -> - {ok, Module, St}; + {ok, Module, St}; {error, Reason} -> ?error("Failed to start reporter ~p: ~p~n", [Module, Reason]), exit(Reason) @@ -1573,23 +1647,23 @@ cast(Req) -> init_subscriber({Reporter, Metric, DataPoint, Interval, RetryFailedMetrics}) -> Status = get_reporter_status(Reporter), subscribe_(Reporter, Metric, DataPoint, Interval, - RetryFailedMetrics, undefined, Status); + RetryFailedMetrics, undefined, Status); init_subscriber({Reporter, Metric, DataPoint, Interval, - RetryFailedMetrics, Extra}) -> + RetryFailedMetrics, Extra}) -> Status = get_reporter_status(Reporter), subscribe_(Reporter, Metric, DataPoint, Interval, - RetryFailedMetrics, Extra, Status); + RetryFailedMetrics, Extra, Status); init_subscriber({Reporter, Metric, DataPoint, Interval}) -> Status = get_reporter_status(Reporter), subscribe_(Reporter, Metric, DataPoint, Interval, - true, undefined, Status); + true, undefined, Status); init_subscriber({apply, {M, F, A}}) -> lists:foreach(fun(Sub) -> - init_subscriber(Sub) - end, apply(M, F, A)); + init_subscriber(Sub) + end, apply(M, F, A)); init_subscriber({select, Expr}) when tuple_size(Expr)==3; - tuple_size(Expr)==4; - tuple_size(Expr)==5 -> + tuple_size(Expr)==4; + tuple_size(Expr)==5 -> {Pattern, Reporter, DataPoint, Interval, Retry, Extra} = case Expr of {P, R, D, I} -> {P, R, D, I, true, undefined}; @@ -1601,23 +1675,23 @@ init_subscriber({select, Expr}) when tuple_size(Expr)==3; lists:foreach( fun({Entry, _, _}) -> subscribe_(Reporter, Entry, DataPoint, Interval, - Retry, Extra, Status) + Retry, Extra, Status) end, Entries); init_subscriber(Other) -> ?warning("Incorrect static subscriber spec ~p. " "Use { Reporter, Metric, DataPoint, Interval [, Extra ]}~n", - [ Other ]). + [ Other ]). get_reporter_status(R) -> try ets:lookup_element(?EXOMETER_REPORTERS, R, #reporter.status) catch - error:_ -> disabled + error:_ -> disabled end. add_restart(#restart{spec = Spec, - history = H, - save_n = N} = R) -> + history = H, + save_n = N} = R) -> T = exometer_util:timestamp(), H1 = lists:sublist([T|H], 1, N), case match_frequency(H1, Spec) of @@ -1671,10 +1745,10 @@ restart_rec(L) -> Save = lists:foldl( fun ({R,_}, Acc) when is_integer(R) -> - erlang:max(R, Acc); + erlang:max(R, Acc); (_, Acc) -> - Acc - end, 0, L), + Acc + end, 0, L), #restart{spec = L, save_n = Save}. valid_restart(L) when is_list(L) -> @@ -1692,37 +1766,37 @@ do_remove_reporter(Reporter) -> do_remove_reporter(Reporter, Terminate) -> case ets:lookup(?EXOMETER_REPORTERS, Reporter) of - [#reporter{} = R] -> + [#reporter{} = R] -> case Terminate of true -> terminate_reporter(R); false -> ok end, - ets:delete(?EXOMETER_REPORTERS, Reporter), - purge_subscriptions(Reporter), - ok; + ets:delete(?EXOMETER_REPORTERS, Reporter), + purge_subscriptions(Reporter), + ok; [] -> {error, not_found} end. change_reporter_status(Reporter, New) -> case ets:lookup(?EXOMETER_REPORTERS, Reporter) of - [R] -> do_change_reporter_status(R, New); - [] -> {error, not_found} + [R] -> do_change_reporter_status(R, New); + [] -> {error, not_found} end. do_change_reporter_status(#reporter{name = Reporter, - status = Old} = R, New) -> + status = Old} = R, New) -> case {Old, New} of - {disabled, enabled} -> - restart_reporter(R); - {enabled, disabled} -> - cancel_subscr_timers(Reporter), - terminate_reporter(R), - ets:update_element(?EXOMETER_REPORTERS, - Reporter, [{#reporter.status, disabled}]); - {Old, Old} -> - ok + {disabled, enabled} -> + restart_reporter(R); + {enabled, disabled} -> + cancel_subscr_timers(Reporter), + terminate_reporter(R), + ets:update_element(?EXOMETER_REPORTERS, + Reporter, [{#reporter.status, disabled}]); + {Old, Old} -> + ok end, ok. diff --git a/src/exometer_report_logger.erl b/src/exometer_report_logger.erl new file mode 100644 index 0000000..5271aea --- /dev/null +++ b/src/exometer_report_logger.erl @@ -0,0 +1,427 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at http://mozilla.org/MPL/2.0/. +%% +%% ------------------------------------------------------------------- +%% @doc Exometer report collector and logger. +%% +%% This module implements a behavior for collecting reporting data and +%% handling it (logging to disk or ets, printing to tty, etc.) +%% +%% The logger has built-in support for receiving input via UDP, TCP or +%% internal Erlang messaging, as well as a plugin API for custom input +%% handling. Correspondingly, it has support for output to TTY or ets, as +%% well as a plugin API for custom output. +%% +%% An example of how the logger can be used can be found in +%% `test/exometer_test_udp_reporter.erl', which implements a UDP-based +%% reporter as well as an input plugin and an output plugin. This reporter +%% is used by `test/exometer_report_SUITE.erl'. +%% +%% Loggers can be combined, e.g. by creating one logger that receives Erlang +%% messages, and other loggers that receive from different sources, prefix +%% their reports and pass them on to the first logger. +%% +%%

Input plugins

+%% +%% An input plugin is initiated by `Module:logger_init_input(State)', where +%% `State' is whatever was given as a `state' option (default: `undefined'). +%% The function must create a process and return `{ok, Pid}'. `Pid' is +%% responsible for setting up whatever input channel is desired, and passes +%% on incoming data to the logger via Erlang messages `{plugin, Pid, Data}'. +%% +%%

Output Chaining

+%% +%% Each incoming data item is passed through the list of output operators. +%% Each output operator is able to modify the data (the `tty' and `ets' +%% operators leave the data unchanged). Output plugins receive the data +%% in `Module:logger_handle_data(Data, State)', which must return +%% `{NewData, NewState}'. The state is private to the plugin, while `NewData' +%% will be passed along to the next output operator. +%% +%%

Flow control

+%% +%% The logger will handle flow control automatically for `udp' and `tcp' +%% inputs. If `{active,once}' or `{active, false}', the logger will trigger +%% `{active, once}' each time it has handled an incoming message. +%% If `{active, N}', it will "refill" the port each time it receives an +%% indication that it has become passive. +%% +%% Input plugins create a process in `Module:logger_init_input/1'. This process +%% can mimick the behavior of Erlang ports by sending a `{plugin_passive, Pid}' +%% message to the logger. The logger will reply with a message, +%% `{plugin_active, N}', where `N' is the value given by the `active' option. +%% @end +-module(exometer_report_logger). + +-behaviour(gen_server). + +-export([new/1]). +-export([start_link/1]). + +-export([info/0, + info/1]). + +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +-record(st, {id, + input, + output}). + +-include_lib("parse_trans/include/exprecs.hrl"). +-export_records([tcp, udp, tty, ets, int]). + +%% input records +-record(tcp, {socket, port, options = [], active = once}). +-record(udp, {socket, port, options = [], active = true}). +%% output records +-record(tty, {prefix = []}). +-record(ets, {tab}). +%% both input and output +-record(int, {process}). +-record(plugin, {module, mod_state, process, active = once}). + +-type proplist() :: [{atom(), any()}]. + +-type logger_info() :: {id, any()} + | {input, proplist()} + | {output, proplist()}. + +-type plugin_state() :: any(). + +-callback logger_init_input(any()) -> + {ok, pid()}. +-callback logger_init_output(any()) -> + {ok, plugin_state()}. +-callback logger_handle_data(binary(), plugin_state()) -> + {binary(), plugin_state()}. + +-spec new([{id, any()} | {input, list()} | {output, list()}]) -> {ok,pid()}. +%% @doc Create a new logger instance. +%% +%% This function creates a logger process with the given input and output +%% parameters. +%% +%% * `{id, ID}' is mainly for documentation and simplifying identification +%% of instances returned by {@link info/0}. +%% * `{input, PropList}' specifies what the logger listens to. Only the first +%% `input' entry is regarded, but the option is mandatory. +%% * `{output, PropList}' specifies what the logger should to with received +%% data. Multiple `output' entries are allowed, and they will be processed +%% in the order given. +%% +%% Valid input options: +%% +%% * `{mode, udp | tcp | internal | plugin}' defines the protocol +%% * `{active, false | true | once | N}' provides flow control. Default: `true'. +%% * (mode-specific options) +%% +%% Valid output options: +%% +%% * `{mode, tty | ets | plugin | internal}' defines output types +%% * (output-specific options) +%% +%% Mode-specific options, `udp': +%% +%% * `{port, integer()}' - UDP port number +%% * `{options, list()}' - Options to pass to {@link gen_udp:open/2} +%% +%% Mode-specific options, `tcp': +%% +%% * `{port, integer()}' - TCP port number +%% * `{options, list()}' - Options to pass to {@link gen_tcp:listen/2} +%% +%% Mode-specific options, `tty': +%% +%% * `{prefix, iolist()}' - Prefix string inserted before the data, which is +%% printed as-is (note that any delimiter would need to be part of the prefix) +%% +%% Mode-specific options, `ets': +%% * `{table, ets:table()}' - Ets table identifier. If not specified, an +%% ordered-set table will be created by the logger process. The incoming +%% data will be inserted as `{erlang:now(), Data}'. +%% +%% Mode-specific options, `internal': +%% * `{process, PidOrRegname}' specifies another logger instance, which is to +%% receive data from this logger (if used in output), or which is allowed +%% to send to this logger (if used in input). If no process is given for +%% input, any process can send data (on the form +%% `{exometer_report_logger, Pid, Data}') to this logger. +%% +%% Mode-specific options, `plugin': +%% +%% * `{module, Module}' - name of callback module +%% (behaviour: `exometer_report_logger') +%% * `{state, State}' - Passed as initial argument to +%% `Module:logger_init_input/1' or `Module:logger_init_output/1', depending +%% on whether the plugin is specified as input or output. +%% @end +new(Options) -> + supervisor:start_child(exometer_report_logger_sup, [Options]). + +-spec start_link(proplist()) -> {ok, pid()}. +%% @doc Start function for logger instance. +%% +%% This function is the start function eventually called as a result from +%% {@link new/1}, but whereas `new/1' creates a supervised instance, this +%% function simply creates the process. It would normally not be used directly. +%% @end +start_link(Options) -> + ID = exometer_util:get_opt(id, Options, undefined), + Input = get_input(Options), + Output = get_output(Options), + gen_server:start_link(?MODULE, {ID, Input, Output}, []). + +-spec info() -> [{pid(), [logger_info()]}]. +%% @doc List active logger instances. +%% +%% This function lists the instances started via {@link new/1}, along with their +%% respective settings as nested property lists. +%% @end +info() -> + [{P, info(P)} || {_, P, _, _} <- supervisor:which_children( + exometer_report_logger_sup)]. +-spec info(pid()) -> [logger_info()]. +%% @doc Lists the settings of a given logger instance. +info(P) -> + gen_server:call(P, info). + +%% Client-side check +get_input(Opts) -> + L = exometer_util:get_opt(input, Opts), + get_input(exometer_util:get_opt(mode, L), L). + +get_input(tcp, L) -> + Port = exometer_util:get_opt(port, L), + Options = exometer_util:get_opt(options, L, []), + Active = exometer_util:get_opt( + active, L, get_opt_active(Options)), + #tcp{port = Port, options = Options, active = Active}; +get_input(udp, L) -> + Port = exometer_util:get_opt(port, L), + Options = exometer_util:get_opt(options, L, []), + Active = exometer_util:get_opt( + active, L, get_opt_active(Options)), + #udp{port = Port, options = Options, active = Active}; +get_input(internal, L) -> + P = exometer_util:get_opt(process, L, undefined), + #int{process = P}; +get_input(plugin, L) -> + Mod = exometer_util:get_opt(module, L), + St = exometer_util:get_opt(state, L, undefined), + Active = exometer_util:get_opt(active, L, true), + #plugin{module = Mod, mod_state = St, active = Active}. + + +get_opt_active(Opts) -> + case lists:keyfind(active, 1, Opts) of + {_, true} -> true; + {_, N} when is_integer(N) -> N; + _ -> once + end. + +%% Client-side check +get_output(Opts) -> + [get_output(exometer_util:get_opt(mode, O), O) || {output, O} <- Opts]. + +get_output(tty, O) -> + Prefix = exometer_util:get_opt(prefix, O, []), + #tty{prefix = Prefix}; +get_output(ets, O) -> + Tab = exometer_util:get_opt(tab, O, undefined), + #ets{tab = Tab}; +get_output(internal, O) -> + P = exometer_util:get_opt(process, O), + #int{process = P}; +get_output(plugin, O) -> + Mod = exometer_util:get_opt(module, O), + St = exometer_util:get_opt(state, O, undefined), + #plugin{module = Mod, mod_state = St}. + + +%% Gen_server callbacks + +%% @private +init({ID, Input, Output}) -> + NewL = init_input(Input), + NewO = init_output(Output), + {ok, #st{id = ID, + input = NewL, + output = NewO}}. + +%% @private +handle_call(info, _, #st{id = ID, input = I, output = O} = S) -> + {reply, info_(ID, I, O), S}; +handle_call(_Req, _From, St) -> + {reply, {error, unsupported}, St}. + +%% @private +handle_cast({socket, Socket}, #st{input = L} = S) -> + case L of + #tcp{active = N} = T -> + case inet:getopts(Socket, [active]) of + {ok, [{active, false}]} -> + inet:setopts(Socket, [{active, N}]); + _ -> + ok + end, + {noreply, S#st{input = T#tcp{socket = Socket}}}; + _ -> + {noreply, S} + end; +handle_cast(_Msg, St) -> + {noreply, St}. + +%% @private +handle_info({tcp, Socket, Data}, #st{input = #tcp{socket = Socket, + active = Active}, + output = Out} = S) -> + handle_data(Data, Out), + check_active(Socket, Active), + {noreply, S}; +handle_info({udp, Socket, _Host, _Port, Data}, + #st{input = #udp{socket = Socket}, output = Out} = S) -> + Out1 = handle_data(Data, Out), + {noreply, S#st{output = Out1}}; +handle_info({plugin, Pid, Data}, #st{input = #plugin{process = Pid}, + output = Out} = S) -> + Out1 = handle_data(Data, Out), + {noreply, S#st{output = Out1}}; +handle_info({tcp_passive, Socket}, #st{input = #tcp{socket = Socket, + active = Active}} = S) -> + inet:setopts(Socket, Active), + {noreply, S}; +handle_info({udp_passive, Socket}, #st{input = #udp{socket = Socket, + active = Active}} = S) -> + inet:setopts(Socket, Active), + {noreply, S}; +handle_info({plugin_passive, Pid}, #st{input = #plugin{process = Pid, + active = Active}} = S) -> + Pid ! {plugin_active, Active}, + {noreply, S}; +handle_info({?MODULE, P, Data}, #st{input = #int{process = Pl}, + output = Out} = S) + when Pl =:= P; Pl =:= undefined -> + Out1 = handle_data(Data, Out), + {noreply, S#st{output = Out1}}; +handle_info(_, S) -> + {noreply, S}. + +%% @private +terminate(_, _) -> + ok. + +%% @private +code_change(_FromVsn, S, _Extra) -> + {ok, S}. + +%% End gen_server callbacks + +info_(ID, I, O) -> + [{id, ID}, + {input, ensure_list(pp(I))}, + {output, ensure_list(pp(O))}]. + +ensure_list(I) when is_tuple(I) -> + [I]; +ensure_list(I) when is_list(I) -> + I. + +%% Copied from git:uwiger/jobs/src/jobs_info.erl +pp(L) when is_list(L) -> + [pp(X) || X <- L]; +pp(X) -> + case '#is_record-'(X) of + true -> + RecName = element(1,X), + {RecName, lists:zip( + '#info-'(RecName,fields), + pp(tl(tuple_to_list(X))))}; + false -> + if is_tuple(X) -> + list_to_tuple(pp(tuple_to_list(X))); + true -> + X + end + end. + +init_input(#tcp{port = Port, + options = Opts} = T) -> + _ = spawn_tcp_acceptor(Port, Opts), + T; +init_input(#udp{port = Port, options = Opts} = U) -> + {ok, Socket} = gen_udp:open(Port, Opts), + U#udp{socket = Socket}; +init_input(#plugin{module = Mod, mod_state = St} = P) -> + case Mod:logger_init_input(St) of + {ok, Pid} when is_pid(Pid) -> + P#plugin{process = Pid} + end; +init_input(#int{} = I) -> + I. + + +spawn_tcp_acceptor(Port, Opts) -> + Parent = self(), + spawn_link(fun() -> + {ok, LSock} = gen_tcp:listen(Port, Opts), + {ok, Socket} = gen_tcp:accept(LSock), + ok = gen_tcp:controlling_process(Socket, Parent), + gen_server:cast(Parent, {socket, Socket}) + end). + +init_output(Out) -> + [init_output_(O) || O <- Out]. + +init_output_(#tty{} = TTY) -> TTY; +init_output_(#int{} = Int) -> Int; +init_output_(#ets{tab = T} = E) -> + Tab = case T of + undefined -> + ets:new(?MODULE, [ordered_set]); + _ -> + T + end, + E#ets{tab = Tab}; +init_output_(#plugin{module = Mod, mod_state = St} = P) -> + {ok, St1} = Mod:logger_init_output(St), + P#plugin{mod_state = St1}. + +check_active(Socket, once) -> + inet:setopts(Socket, [{active, once}]); +check_active(_Socket, _) -> + ok. + +handle_data(Data, Out) -> + handle_data(Data, Out, []). + +handle_data(Data, [H|T], Acc) -> + {Data1, H1} = handle_data_(Data, H), + handle_data(Data1, T, [H1|Acc]); +handle_data(_, [], Acc) -> + lists:reverse(Acc). + +handle_data_(Data, #tty{prefix = Pfx} = Out) -> + io:fwrite(iolist_to_binary([Pfx, Data, $\n])), + {Data, Out}; +handle_data_(Data, #ets{tab = T} = Out) -> + ets:insert(T, {erlang:now(), Data}), + {Data, Out}; +handle_data_(Data, #int{process = P} = Out) -> + try P ! {?MODULE, self(), Data} catch _:_ -> error end, + {Data, Out}; +handle_data_(Data, #plugin{module = Mod, mod_state = ModSt} = Out) -> + {Data1, ModSt1} = Mod:logger_handle_data(Data, ModSt), + {Data1, Out#plugin{mod_state = ModSt1}}. + + + diff --git a/src/exometer_report_logger_sup.erl b/src/exometer_report_logger_sup.erl new file mode 100644 index 0000000..0031ced --- /dev/null +++ b/src/exometer_report_logger_sup.erl @@ -0,0 +1,27 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at http://mozilla.org/MPL/2.0/. +%% +%% ------------------------------------------------------------------- +%% @private +-module(exometer_report_logger_sup). + +-behaviour(supervisor). + +-export([start_link/0]). + +-export([init/1]). + +-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). + +start_link() -> + supervisor:start_link({local,?MODULE}, ?MODULE, []). + + +init(_) -> + {ok, {{simple_one_for_one, 3, 10}, + [?CHILD(exometer_report_logger, worker)]}}. diff --git a/src/exometer_shallowtree.erl b/src/exometer_shallowtree.erl index ded57e3..354bbf0 100644 --- a/src/exometer_shallowtree.erl +++ b/src/exometer_shallowtree.erl @@ -12,20 +12,20 @@ -module(exometer_shallowtree). -export([new/1, - insert/3, - take_min/1, - to_list/1, - filter/2, - size/1, - limit/1]). + insert/3, + take_min/1, + to_list/1, + filter/2, + size/1, + limit/1]). -export([fill/1, fill1/2]). -export_type([tree/0]). -record(t, {size = 0, - limit = 10, - tree = []}). + limit = 10, + tree = []}). -type tree() :: #t{}. @@ -52,11 +52,11 @@ limit(#t{limit = L}) -> %% @end insert(K, V, #t{size = X, limit = X, tree = Tr} = T) when is_number(K) -> case K =< element(1, Tr) of - true -> - T; - false -> - {_, _, Tr1} = take_min_(Tr), - T#t{tree = insert_(K, V, Tr1)} + true -> + T; + false -> + {_, _, Tr1} = take_min_(Tr), + T#t{tree = insert_(K, V, Tr1)} end; insert(K, V, #t{size = Sz, tree = Tr} = T) when is_number(K) -> T#t{size = Sz+1, tree = insert_(K, V, Tr)}. @@ -71,9 +71,9 @@ insert_(K, V, T ) -> meld(mknode(K, V), T). %% @end take_min(#t{size = Sz, tree = Tr} = T) -> case take_min_(Tr) of - error -> error; - {K, V, Tr1} -> - {K, V, T#t{size = Sz-1, tree = Tr1}} + error -> error; + {K, V, Tr1} -> + {K, V, T#t{size = Sz-1, tree = Tr1}} end. take_min_([]) -> error; @@ -99,16 +99,16 @@ filter(F, #t{tree = T}) -> filter_(F, [T]). filter_(_, []) -> []; filter_(F, [{K,V,_,L,R}|T]) -> case F(K,V) of false -> filter_(F, [L,R|T]); - {true, Keep} -> [Keep|filter_(F, [L,R|T])] + {true, Keep} -> [Keep|filter_(F, [L,R|T])] end; filter_(F, [[]|T]) -> filter_(F, T). meld({K1,V1, _, L1, R1} = T1, {K2,V2, _, L2, R2} = T2) -> case K1 < K2 of - true -> - mknode(K1,V1, L1, meld(R1, T2)); - false -> - mknode(K2,V2, L2, meld(R2, T1)) + true -> + mknode(K1,V1, L1, meld(R1, T2)); + false -> + mknode(K2,V2, L2, meld(R2, T1)) end; meld([], T2) -> T2; meld(T1, []) -> T1; diff --git a/src/exometer_slide.erl b/src/exometer_slide.erl index 4f9b4de..a712431 100644 --- a/src/exometer_slide.erl +++ b/src/exometer_slide.erl @@ -178,7 +178,7 @@ to_list(#slide{size = Sz, n = N, max_n = MaxN, buf1 = Buf1, buf2 = Buf2}) -> foldl(_Timestamp, _Fun, _Acc, #slide{size = Sz}) when Sz == 0 -> []; foldl(Timestamp, Fun, Acc, #slide{size = Sz, n = N, max_n = MaxN, - buf1 = Buf1, buf2 = Buf2}) -> + buf1 = Buf1, buf2 = Buf2}) -> Start = Timestamp - Sz, lists:foldr( Fun, lists:foldl(Fun, Acc, take_since( diff --git a/src/exometer_slot_slide.erl b/src/exometer_slot_slide.erl index eb3f381..e3ac953 100644 --- a/src/exometer_slot_slide.erl +++ b/src/exometer_slot_slide.erl @@ -177,7 +177,7 @@ -export([new/2, new/4, new/5, add_element/2, add_element/3, - add_element/4, + add_element/4, reset/1, to_list/1, foldl/3, @@ -265,7 +265,7 @@ add_element(TS, Val, #slide{cur_slot = CurrentSlot, %% Invoke the sample MFA to get a new state to work with %% ret(Wrap, Flag, - Slide1#slide {cur_state = SampleF(TS, Val, Slide1#slide.cur_state)}). + Slide1#slide {cur_state = SampleF(TS, Val, Slide1#slide.cur_state)}). ret(false, _, Slide) -> Slide; @@ -313,8 +313,8 @@ foldr(Fun, Acc, Slide) -> %% within the timespan ranging from Oldest up to the current timme. %% take_since(Oldest, #slide{%% cur_slot = CurrentSlot, - cur_state = CurrentState, - slot_period = SlotPeriod } =Slide) -> + cur_state = CurrentState, + slot_period = SlotPeriod } =Slide) -> %% Check if we need to add a slot for the current time period %% before we start to grab data. @@ -325,9 +325,9 @@ take_since(Oldest, #slide{%% cur_slot = CurrentSlot, #slide { list1 = List1, list2 = List2} = %% if TSSlot =/= CurrentSlot, CurrentState =/= undefined -> - if CurrentState =/= undefined -> + if CurrentState =/= undefined -> {_, Sl} = add_slot(TS, Slide), - Sl; + Sl; true -> Slide end, @@ -368,7 +368,7 @@ add_slot(TS, #slide{timespan = TimeSpan, undefined -> %% Transformation function could not produce an element %% Reset the time slot to the current slot. Reset state./ {false, Slide#slide{ cur_slot = TSSlot, - cur_state = undefined}}; + cur_state = undefined}}; %% The transform function produced an element to store Element -> @@ -380,16 +380,16 @@ add_slot(TS, #slide{timespan = TimeSpan, %% Shift list1 into list2. %% Add the new slot as the initial element of list1 {true, Slide#slide{ list1 = [{ CurrentSlot, Element }], - list2 = List1, - list1_start_slot = CurrentSlot, - cur_slot = TSSlot, - cur_state = undefined}}; + list2 = List1, + list1_start_slot = CurrentSlot, + cur_slot = TSSlot, + cur_state = undefined}}; true -> %% No shift necessary. Tack on the new slot to list1. {false, - Slide#slide{list1 = [{CurrentSlot, Element} | List1], - cur_slot = TSSlot, - cur_state = undefined}} + Slide#slide{list1 = [{CurrentSlot, Element} | List1], + cur_slot = TSSlot, + cur_state = undefined}} end end. diff --git a/src/exometer_spiral.erl b/src/exometer_spiral.erl index a4bbb9d..17aadce 100644 --- a/src/exometer_spiral.erl +++ b/src/exometer_spiral.erl @@ -13,16 +13,16 @@ %% exometer_entry callbacks -export([behaviour/0, - probe_init/3, - probe_terminate/1, - probe_setopts/3, - probe_update/2, - probe_get_value/2, - probe_get_datapoints/1, - probe_reset/1, - probe_code_change/3, - probe_sample/1, - probe_handle_msg/2]). + probe_init/3, + probe_terminate/1, + probe_setopts/3, + probe_update/2, + probe_get_value/2, + probe_get_datapoints/1, + probe_reset/1, + probe_code_change/3, + probe_sample/1, + probe_handle_msg/2]). %% exometer_proc callback -export([count_sample/3, @@ -64,7 +64,7 @@ probe_init(Name, _Type, Options) -> {ok, St#st{slide = Slide}}. probe_terminate(_St) -> - ok. + ok. probe_get_value(DataPoints, St) -> {ok, [get_single_value(St, DataPoint) || DataPoint <- DataPoints]}. @@ -77,8 +77,8 @@ probe_setopts(_Entry, _Options, _St) -> probe_update(Increment, #st{slide = Slide, total = Total} = St) -> {ok, St#st{ - slide = exometer_slot_slide:add_element(Increment, Slide), - total = Total + Increment}}. + slide = exometer_slot_slide:add_element(Increment, Slide), + total = Total + Increment}}. probe_reset(#st{slide = Slide} = St) -> {ok, St#st{total = 0, slide = exometer_slot_slide:reset(Slide)}}. @@ -104,14 +104,14 @@ process_opts(St, Options) -> %% Unknown option, pass on to State options list, replacing %% any earlier versions of the same option. ({Opt, Val}, St1) -> - St1#st{opts = [{Opt, Val} - | lists:keydelete(Opt, 1, St1#st.opts)]} - end, St, Options). + St1#st{opts = [{Opt, Val} + | lists:keydelete(Opt, 1, St1#st.opts)]} + end, St, Options). %% Simple sample processor that maintains a counter. %% of all count_sample(_TS, Increment, undefined) -> - Increment; + Increment; count_sample(_TS, Increment, Total) -> Total + Increment. @@ -130,6 +130,6 @@ get_single_value(St, count) -> get_single_value(St, one) -> {one, exometer_slot_slide:foldl(fun({_TS, Val}, Acc) -> Acc + Val end, - 0, St#st.slide) }; + 0, St#st.slide) }; get_single_value(_St, Unsupported) -> {Unsupported, {error, unsupported}}. diff --git a/src/exometer_uniform.erl b/src/exometer_uniform.erl index 79bae21..d716461 100644 --- a/src/exometer_uniform.erl +++ b/src/exometer_uniform.erl @@ -14,7 +14,7 @@ %% exometer_probe callbacks -export([behaviour/0, - probe_init/3, + probe_init/3, probe_terminate/1, probe_get_value/2, probe_get_datapoints/1, @@ -67,9 +67,9 @@ probe_terminate(ModSt) -> probe_get_value(DataPoints, St) -> {Length, Total, Lst} = ets:foldl( - fun(#elem { val = Val }, {Length, Total, List}) -> - { Length + 1, Total + Val, [ Val | List ]} end, - {0, 0.0, []}, St#st.ets_ref), + fun(#elem { val = Val }, {Length, Total, List}) -> + { Length + 1, Total + Val, [ Val | List ]} end, + {0, 0.0, []}, St#st.ets_ref), Sorted = lists:sort(Lst), Mean = case Length of @@ -124,9 +124,9 @@ process_opts(St, Options) -> %% Unknown option, pass on to State options list, replacing %% any earlier versions of the same option. ({Opt, Val}, St1) -> - St1#st{ opts = [ {Opt, Val} - | lists:keydelete(Opt, 1, St1#st.opts) ] } - end, St, Options). + St1#st{ opts = [ {Opt, Val} + | lists:keydelete(Opt, 1, St1#st.opts) ] } + end, St, Options). probe_handle_msg(_, S) -> diff --git a/src/exometer_util.erl b/src/exometer_util.erl index dda73fd..04965ef 100644 --- a/src/exometer_util.erl +++ b/src/exometer_util.erl @@ -15,6 +15,7 @@ [ timestamp/0, timestamp_to_datetime/1, + get_opt/2, get_opt/3, get_env/2, tables/0, @@ -22,6 +23,7 @@ get_statistics/3, get_statistics2/4, pick_items/2, + perc/2, histogram/1, histogram/2, drop_duplicates/1, @@ -32,7 +34,8 @@ set_status/2, set_event_flag/2, clear_event_flag/2, - test_event_flag/2 + test_event_flag/2, + ensure_all_started/1 ]). -export_type([timestamp/0]). @@ -58,7 +61,7 @@ timestamp() -> (MS-1258)*1000000000 + S*1000 + US div 1000. -spec timestamp_to_datetime(timestamp()) -> - {calendar:datetime(), non_neg_integer()}. + {calendar:datetime(), non_neg_integer()}. %% @doc Convert timestamp to a regular datetime. %% %% The timestamp is expected @@ -76,18 +79,25 @@ get_env(Key, Default) -> {ok, Value} -> Value; _ -> - case get_env1(exometer_core, Key) of - {ok, CoreValue} -> - CoreValue; - _ -> - Default - end + case get_env1(exometer_core, Key) of + {ok, CoreValue} -> + CoreValue; + _ -> + Default + end end. get_env1(App, Key) -> case application:get_env(App, Key) of - {ok, undefined} -> undefined; - Other -> Other + {ok, undefined} -> undefined; + Other -> Other + end. + +get_opt(K, Opts) -> + case lists:keyfind(K, 1, Opts) of + {_, V} -> V; + false -> + error({required, K}) end. get_opt(K, Opts, Default) -> @@ -182,15 +192,15 @@ drop_duplicates(List0) when is_list(List0) -> List1 = lists:foldl( fun (Elem, Acc) when is_tuple(Elem) -> - case lists:keymember(element(1, Elem), 1, Acc) of - true -> - Acc; - false -> - [Elem | Acc] - end; + case lists:keymember(element(1, Elem), 1, Acc) of + true -> + Acc; + false -> + [Elem | Acc] + end; (_, Acc) -> - Acc - end, [], List0), + Acc + end, [], List0), lists:reverse(List1); drop_duplicates(Any) -> Any. @@ -206,7 +216,7 @@ histogram(Values, default) -> histogram(Values, DataPoints) -> H = histogram(Values), [DP || {K,_} = DP <- H, - lists:member(K, DataPoints)]. + lists:member(K, DataPoints)]. -spec get_statistics(Length::non_neg_integer(), Total::non_neg_integer(), @@ -247,10 +257,10 @@ get_statistics2(L, Sorted, Total, Mean) -> Items = [{min,1}, {50, P50}, {median, P50}, {75, perc(0.75,L)}, {90, perc(0.9,L)}, {95, perc(0.95,L)}, {99, perc(0.99,L)}, {999, perc(0.999,L)}, {max,L}], - [{n,L}, {mean, Mean}, {total, Total} | pick_items(Sorted, 1, Items)]. + [{n,L}, {mean, Mean}, {total, Total} | pick_items(Sorted, Items)]. -spec pick_items([number()], [{atom() | integer(), integer()}]) -> - [{atom(), number()}]. + [{atom(), number()}]. %% @doc Pick values from specified positions in a sorted list of numbers. %% %% This function is used to extract datapoints (usually percentiles) from @@ -304,16 +314,16 @@ key_match(_, _) -> false. get_datapoints(#exometer_entry{module = exometer, - type = T}) when T==counter; + type = T}) when T==counter; T==fast_counter; T==gauge -> [value, ms_since_reset]; get_datapoints(#exometer_entry{behaviour = entry, - name = Name, module = M, - type = Type, ref = Ref}) -> + name = Name, module = M, + type = Type, ref = Ref}) -> M:get_datapoints(Name, Type, Ref); get_datapoints(#exometer_entry{behaviour = probe, - name = Name, type = Type, ref = Ref}) -> + name = Name, type = Type, ref = Ref}) -> exometer_probe:get_datapoints(Name, Type, Ref). set_call_count({M, F}, Bool) -> @@ -326,9 +336,9 @@ get_status(enabled) -> enabled; get_status(disabled) -> disabled; get_status(St) when is_integer(St) -> if St band 2#1 == 1 -> - enabled; + enabled; true -> - disabled + disabled end. set_status(enabled , enabled ) -> 1; @@ -352,6 +362,35 @@ clear_event_flag(update, disabled) -> 0. test_event_flag(update, St) when St band 2#10 =:= 2#10 -> true; test_event_flag(update, _) -> false. +%% This implementation is originally from Basho's Webmachine. On +%% older versions of Erlang, we don't have +%% application:ensure_all_started, so we use this wrapper function to +%% either use the native implementation or our own version, depending +%% on what's available. +-spec ensure_all_started(atom()) -> {ok, [atom()]} | {error, term()}. +ensure_all_started(App) -> + %% Referencing application:ensure_all_started/1 will anger Xref + %% in earlier R16B versions of OTP + ensure_all_started(App, []). + +%% This implementation is originally from Basho's +%% Webmachine. Reimplementation of ensure_all_started. NOTE this does +%% not behave the same as the native version in all cases, but as a +%% quick hack it works well enough for our purposes. Eventually I +%% assume we'll drop support for older versions of Erlang and this can +%% be eliminated. +ensure_all_started(App, Apps0) -> + case application:start(App) of + ok -> + {ok, lists:reverse([App | Apps0])}; + {error,{already_started,App}} -> + {ok, lists:reverse(Apps0)}; + {error,{not_started,BaseApp}} -> + {ok, Apps} = ensure_all_started(BaseApp, Apps0), + ensure_all_started(App, [BaseApp|Apps]) + end. + + %% EUnit tests -ifdef(TEST). @@ -363,7 +402,7 @@ key_match_test() -> {ok,yes} = report_type([a,b,c], [], [{[a,b,'_'], yes}]), {ok,yes} = report_type([a,b,c], [], [{[a,'_',c], yes}]), {ok,yes} = report_type([a,b,c], [], [{[a,b|'_'], yes}]), - {ok,yes} = report_type([a,b,c], [{type,yes}], [{[a,b,c], no}]), + {ok,yes} = report_type([a,b,c], [{report_type,yes}], [{[a,b,c], no}]), ok. -endif. diff --git a/test/ct.config b/test/ct.config new file mode 100644 index 0000000..62f5cfa --- /dev/null +++ b/test/ct.config @@ -0,0 +1,8 @@ +%% -*- erlang -*- +{lager, [ + {handlers, [ + {lager_console_backend, debug}, + {lager_file_backend, [{file, "error.log"}, {level, error}]}, + {lager_file_backend, [{file, "console.log"}, {level, debug}]} + ]} +]}. diff --git a/test/exometer_SUITE.erl b/test/exometer_SUITE.erl index 8223754..2ecb276 100644 --- a/test/exometer_SUITE.erl +++ b/test/exometer_SUITE.erl @@ -23,10 +23,12 @@ test_std_counter/1, test_gauge/1, test_fast_counter/1, + test_wrapping_counter/1, test_update_or_create/1, test_update_or_create2/1, test_default_override/1, test_std_histogram/1, + test_slot_histogram/1, test_std_duration/1, test_folsom_histogram/1, test_aggregate/1, @@ -47,6 +49,8 @@ vals/0 ]). +-import(exometer_test_util, [majority/2]). + -include_lib("common_test/include/ct.hrl"). %%%=================================================================== @@ -67,7 +71,8 @@ groups() -> [ test_std_counter, test_gauge, - test_fast_counter + test_fast_counter, + test_wrapping_counter ]}, {test_defaults, [shuffle], [ @@ -78,6 +83,7 @@ groups() -> {test_histogram, [shuffle], [ test_std_histogram, + test_slot_histogram, test_std_duration, test_folsom_histogram, test_aggregate, @@ -109,52 +115,65 @@ init_per_testcase(Case, Config) when Case == test_folsom_histogram; Case == test_history1_folsom; Case == test_history4_folsom -> + {ok, StartedApps} = exometer_test_util:ensure_all_started(exometer_core), application:start(bear), application:start(folsom), - exometer:start(), - Config; + [{started_apps, StartedApps} | Config]; init_per_testcase(Case, Config) when Case == test_ext_predef; Case == test_function_match -> ok = application:set_env(stdlib, exometer_predefined, {script, "../../test/data/test_defaults.script"}), - ok = application:start(setup), - exometer:start(), - Config; + {ok, StartedApps} = exometer_test_util:ensure_all_started(exometer_core), + ct:log("StartedApps = ~p~n", [StartedApps]), + [{started_apps, StartedApps} | Config]; init_per_testcase(test_app_predef, Config) -> compile_app1(Config), - exometer:start(), + {ok, StartedApps} = exometer_test_util:ensure_all_started(exometer_core), + ct:log("StartedApps = ~p~n", [StartedApps]), Scr = filename:join(filename:dirname( - filename:absname(?config(data_dir, Config))), - "data/app1.script"), + filename:absname(?config(data_dir, Config))), + "data/app1.script"), ok = application:set_env(app1, exometer_predefined, {script, Scr}), - Config; + [{started_apps, StartedApps} | Config]; init_per_testcase(_Case, Config) -> - exometer:start(), - Config. + {ok, StartedApps} = exometer_test_util:ensure_all_started(exometer_core), + ct:log("StartedApps = ~p~n", [StartedApps]), + [{started_apps, StartedApps} | Config]. -end_per_testcase(Case, _Config) when +end_per_testcase(Case, Config) when Case == test_folsom_histogram; Case == test_history1_folsom; Case == test_history4_folsom -> - exometer:stop(), + _ = stop_started_apps(Config), folsom:stop(), application:stop(bear), ok; -end_per_testcase(Case, _Config) when +end_per_testcase(Case, Config) when Case == test_ext_predef; Case == test_function_match -> - ok = application:unset_env(common_test, exometer_predefined), - exometer:stop(), - ok = application:stop(setup), + ok = application:unset_env(stdlib, exometer_predefined), + _ = stop_started_apps(Config), ok; -end_per_testcase(test_app_predef, _Config) -> +end_per_testcase(test_app_predef, Config) -> + ok = application:unset_env(app1, exometer_predefined), ok = application:stop(app1), - exometer:stop(), + _ = stop_started_apps(Config), ok; -end_per_testcase(_Case, _Config) -> - exometer:stop(), +end_per_testcase(_Case, Config) -> + _ = stop_started_apps(Config), ok. +stop_started_apps(Config) -> + [stop_app(App) || + App <- lists:reverse(?config(started_apps, Config))]. + +stop_app(App) -> + case application:stop(App) of + ok -> ok; + {error, {not_started, _}} -> + ok + end. + %%%=================================================================== %%% Test Cases %%%=================================================================== @@ -190,6 +209,25 @@ test_fast_counter(_Config) -> {ok, [{value, 2}, {ms_since_reset, _}]} = exometer:get_value(C), ok. +test_wrapping_counter(_Config) -> + C = [?MODULE, ctr, ?LINE], + ok = exometer:new(C, counter, []), + Max16 = 65534, + Max32 = 4294967294, + Max64 = 18446744073709551614, + Max64p1 = 18446744073709551615, + Max64p21 = 18446744073709551635, + ok = exometer:update(C, Max64), + {ok, [{value, Max64}, {value16, Max16}, {value32, Max32}, {value64, Max64}]} = + exometer:get_value(C, [value, value16, value32, value64]), + ok = exometer:update(C, 1), + {ok, [{value, Max64p1}, {value16, 0}, {value32, 0}, {value64, 0}]} = + exometer:get_value(C, [value, value16, value32, value64]), + [ok = exometer:update(C, 1) || _ <- lists:seq(1, 20)], + {ok, [{value, Max64p21}, {value16, 20}, {value32, 20}, {value64, 20}]} = + exometer:get_value(C, [value, value16, value32, value64]), + ok. + test_update_or_create(_Config) -> {error, not_found} = exometer:update([a,b,c], 2), {error, no_template} = exometer:update_or_create([a,b,c], 10), @@ -240,6 +278,24 @@ test_std_histogram(_Config) -> {50,2},{75,3},{90,4},{95,5},{99,8},{999,9}] = scale_mean(DPs), ok. +test_slot_histogram(Config) -> + C = [?MODULE, hist, ?LINE], + majority(fun test_slot_histogram_/1, [{metric_name, C}|Config]). + +test_slot_histogram_({cleanup, Config}) -> + C = ?config(metric_name, Config), + exometer:delete(C); +test_slot_histogram_(Config) -> + C = ?config(metric_name, Config), + ok = exometer:new(C, histogram, [{histogram_module, exometer_slot_slide}, + {keep_high, 100}, + {truncate, false}]), + [ok = update_(C,V) || V <- vals()], + {_, {ok,DPs}} = timer:tc(exometer, get_value, [C]), + [{n,_},{mean,2126866},{min,1},{max,9},{median,2}, + {50,2},{75,3},{90,4},{95,5},{99,8},{999,9}] = scale_mean(DPs), + ok. + test_std_duration(_Config) -> C = [?MODULE, dur, ?LINE], ok = exometer:new(C, duration, []), diff --git a/test/exometer_alias_SUITE.erl b/test/exometer_alias_SUITE.erl new file mode 100644 index 0000000..66c7c51 --- /dev/null +++ b/test/exometer_alias_SUITE.erl @@ -0,0 +1,168 @@ +-module(exometer_alias_SUITE). + +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2014 Basho Technologies, Inc. All Rights Reserved. +%% +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at http://mozilla.org/MPL/2.0/. +%% +%% ------------------------------------------------------------------- +%% common_test exports +-export( + [ + all/0, groups/0, suite/0, + init_per_suite/1, end_per_suite/1, + init_per_testcase/2, end_per_testcase/2 + ]). + +%% test case exports +-export( + [ + test_register_alias/1, + test_load_map/1, + test_unload_map/1, + test_map_duplicates/1, + test_map_badargs/1, + test_map_existing/1, + test_map_multiple_errors/1 + ]). + +-include_lib("common_test/include/ct.hrl"). + +%%%=================================================================== +%%% common_test API +%%%=================================================================== + +all() -> + [ + {group, test_alias}, + {group, test_map} + ]. + +groups() -> + [ + {test_alias, [shuffle], + [ + test_register_alias + ]}, + {test_map, [shuffle], + [ + test_load_map, + test_unload_map, + test_map_duplicates, + test_map_badargs, + test_map_existing, + test_map_multiple_errors + ]} + ]. + +suite() -> + []. + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +init_per_testcase(Case, Config) -> + {ok, StartedApps} = exometer_test_util:ensure_all_started(exometer_core), + [{started_apps, StartedApps} | Config]. + +end_per_testcase(_Case, Config) -> + [application:stop(App) || App <- ?config(started_apps, Config)], + ok. + +%%%=================================================================== +%%% Test Cases +%%%=================================================================== +test_register_alias(_Config) -> + M = [?MODULE, ?LINE], + ok = exometer_alias:new(t1, M, value), + {M, value} = exometer_alias:resolve(t1), + ok = exometer_alias:delete(t1), + error = exometer_alias:resolve(t1), + ok. + +test_load_map(_Config) -> + ok = exometer_alias:load(fun map1/0), + {[map1,1], a} = exometer_alias:resolve(m11a), + {[map1,1], b} = exometer_alias:resolve(m11b), + {[map1,2], a} = exometer_alias:resolve(m12a), + {[map1,2], b} = exometer_alias:resolve(m12b), + ok. + +test_unload_map(Config) -> + ok = test_load_map(Config), + ok = exometer_alias:unload(fun map1/0), + error = exometer_alias:resolve(m11a), + error = exometer_alias:resolve(m11b), + error = exometer_alias:resolve(m12a), + error = exometer_alias:resolve(m12b), + ok. + +test_map_duplicates(_Config) -> + {error, {map_error, [{duplicate_aliases, + [{m21a, [{[map2,1], a}, + {[map2,1], b}]}] + }]}} = + exometer_alias:load(fun map2/0), + ok. + +test_map_badargs(_Config) -> + {error, {map_error, [{invalid, + [{m31a,[{{map3,1},a}]}] + }]}} = + exometer_alias:load(fun map3/0), + ok. + +test_map_existing(_Config) -> + exometer_alias:new(m11a, [some,metric], a), + {error, {map_error, [{existing_aliases, + [{m11a, [{[some,metric], a}]}]} + ]}} = + exometer_alias:load(fun map1/0), + ok. + +test_map_multiple_errors(_Config) -> + exometer_alias:new(m11a, [some,other,metric], a), + Map = fun() -> + map1() ++ map2() ++ map3() + end, + {error, {map_error, Errors}} = exometer_alias:load(Map), + [ + {duplicate_aliases, [ + {m21a,[{[map2,1],a},{[map2,1], b}]} + ]}, + {existing_aliases, [ + {m11a, [{[some,other,metric], a}]} + ]}, + {invalid, [ + {m31a, [{{map3,1},a}]} + ]} + ] = lists:sort(Errors), + ok. + +map1() -> + [ + {[map1, 1], [{a, m11a}, + {b, m11b}]}, + {[map1, 2], [{a, m12a}, + {b, m12b}]} + ]. + +%% contains error: duplicate aliases +map2() -> + [ + {[map2, 1], [{a, m21a}, + {b, m21a}]}, + {[map2, 2], [{a, m22a}, + {b, m22b}]} + ]. + +map3() -> + [ + {{map3, 1}, [{a, m31a}]} + ]. diff --git a/test/exometer_error_SUITE.erl b/test/exometer_error_SUITE.erl new file mode 100644 index 0000000..d3a50f8 --- /dev/null +++ b/test/exometer_error_SUITE.erl @@ -0,0 +1,186 @@ +-module(exometer_error_SUITE). + +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2014 Basho Technologies, Inc. All Rights Reserved. +%% +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at http://mozilla.org/MPL/2.0/. +%% +%% ------------------------------------------------------------------- +%% common_test exports +-export( + [ + all/0, groups/0, suite/0, + init_per_suite/1, end_per_suite/1, + init_per_testcase/2, end_per_testcase/2 + ]). + +%% test case exports +-export( + [ + test_failing_probe/1, + test_escalation_1/1, + test_escalation_2/1 + ]). + +-import(exometer_test_util, [majority/2]). + +-include_lib("common_test/include/ct.hrl"). + +%%%=================================================================== +%%% common_test API +%%%=================================================================== + +all() -> + [ + {group, test_probes} + ]. + +groups() -> + [ + {test_probes, [shuffle], + [ + test_failing_probe, + test_escalation_1, + test_escalation_2 + ]} + ]. + +suite() -> + []. + +init_per_suite(Config) -> + _ = application:stop(exometer_core), + Config. + +end_per_suite(_Config) -> + ok. + +init_per_testcase(_Case, Config) -> + {ok, Started} = exometer_test_util:ensure_all_started(exometer_core), + ct:log("Started: ~p~n", [[{T, catch ets:tab2list(T)} + || T <- exometer_util:tables()]]), + [{started_apps, Started}|Config]. + +end_per_testcase(_Case, Config) -> + ct:log("end_per_testcase(Config = ~p)~n", [Config]), + stop_started_apps(Config), + ok. + +stop_started_apps(Config) -> + [stop_app(A) || A <- lists:reverse(?config(started_apps, Config))], + ok. + +stop_app(App) -> + case application:stop(App) of + ok -> ok; + {error, {not_started,_}} -> ok + end. + +%%%=================================================================== +%%% Test Cases +%%%=================================================================== +test_failing_probe(Config) -> + M = [?MODULE, ?LINE], + majority(fun test_failing_probe_/1, [{metric_name, M}|Config]). + +test_failing_probe_({cleanup, Config}) -> + M = ?config(metric_name, Config), + exometer:delete(M); +test_failing_probe_(Config) -> + M = ?config(metric_name, Config), + ok = exometer:new(M, histogram, []), + true = killed_probe_restarts(M), + ok. + +test_escalation_1(Config) -> + M = [?MODULE, ?LINE], + majority(fun test_escalation_1_/1, [{metric_name, M}|Config]). + +test_escalation_1_({cleanup, Config}) -> + exometer:delete(?config(metric_name, Config)); +test_escalation_1_(Config) -> + M = ?config(metric_name, Config), + Levels = [{{3,5000}, restart}, + {'_', disable}], + ok = exometer:new(M, histogram, [{restart, Levels}]), + true = killed_probe_restarts(M), + true = killed_probe_restarts(M), + true = killed_probe_restarts(M), + true = killed_probe_disabled(M), + ok. + +test_escalation_2(Config) -> + M = [?MODULE, ?LINE], + majority(fun test_escalation_2_/1, [{metric_name, M}|Config]). + +test_escalation_2_({cleanup, Config}) -> + exometer:delete(?config(metric_name, Config)); +test_escalation_2_(Config) -> + M = ?config(metric_name, Config), + Levels = [{{3,5000}, restart}, + {'_', delete}], + ok = exometer:new(M, histogram, [{restart, Levels}]), + true = killed_probe_restarts(M), + true = killed_probe_restarts(M), + true = killed_probe_restarts(M), + true = killed_probe_deleted(M), + ok. + +killed_probe_restarts(M) -> + Pid = exometer:info(M, ref), + ct:log("Pid = ~p~n", [Pid]), + exit(Pid, kill), + await_death(Pid), + NewPid = exometer:info(M, ref), + ct:log("NewPid = ~p~n", [NewPid]), + enabled = exometer:info(M, status), + true = Pid =/= NewPid. + +killed_probe_disabled(M) -> + Pid = exometer:info(M, ref), + ct:log("Pid = ~p~n", [Pid]), + exit(Pid, kill), + await_death(Pid), + undefined = exometer:info(M, ref), + ct:log("Ref = undefined~n", []), + disabled = exometer:info(M, status), + true. + +killed_probe_deleted(M) -> + Pid = exometer:info(M, ref), + ct:log("Pid = ~p~n", [Pid]), + exit(Pid, kill), + await_death(Pid), + ct:log("Ets = ~p~n", [[{T,ets:tab2list(T)} || + T <- exometer_util:tables()]]), + {error, not_found} = exometer:get_value(M), + ct:log("~p deleted~n", [M]), + true. + +await_death(Pid) -> + Ref = erlang:send_after(1000, self(), zombie), + await_death(Pid, Ref), + %% Now ping exometer_admin twice to give it time to handle the DOWN msg + sys:get_status(exometer_admin), + sys:get_status(exometer_admin), + ok. + +await_death(Pid, Ref) -> + case erlang:read_timer(Ref) of + false -> + error({process_not_dead, Pid}); + _ -> + case erlang:is_process_alive(Pid) of + true -> + erlang:bump_reductions(500), + await_death(Pid, Ref); + false -> + erlang:cancel_timer(Ref), + sys:get_status(exometer_admin), + sys:get_status(exometer_admin), + ok + end + end. diff --git a/test/exometer_report_SUITE.erl b/test/exometer_report_SUITE.erl new file mode 100644 index 0000000..d9b577c --- /dev/null +++ b/test/exometer_report_SUITE.erl @@ -0,0 +1,244 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at http://mozilla.org/MPL/2.0/. +%% +%% ------------------------------------------------------------------- +-module(exometer_report_SUITE). + +%% common_test exports +-export( + [ + all/0, groups/0, suite/0, + init_per_suite/1, end_per_suite/1, + init_per_testcase/2, end_per_testcase/2 + ]). + +%% test case exports +-export( + [ + test_newentry/1, + test_subscribe/1, + test_subscribe_find/1, + test_subscribe_select/1, + test_logger_flow_control/1, + test_logger_flow_control_2/1 + ]). + +-behaviour(exometer_report_logger). +-export([logger_init_output/1, + logger_handle_data/2]). + +-import(exometer_test_util, [majority/2]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("exometer_core/include/exometer.hrl"). + +-define(DEFAULT_PORT, 8888). + +all() -> + [ + {group, test_reporter}, + {group, test_logger} + ]. + +groups() -> + [ + {test_reporter, [shuffle], + [ + test_newentry, + test_subscribe, + test_subscribe_find, + test_subscribe_select + ]}, + {test_logger, [], + [ + test_logger_flow_control, + test_logger_flow_control_2 + ]} + ]. + +suite() -> + []. + +init_per_suite(Config) -> + Config. + +init_per_testcase(_Case, Config) -> + {ok, StartedApps} = exometer_test_util:ensure_all_started(exometer_core), + [{started_apps, StartedApps} | Config]. + +end_per_testcase(_Case, Config) -> + stop_started_apps(Config). + +end_per_suite(_Config) -> + ok. + +stop_started_apps(Config) -> + [application:stop(App) || + App <- lists:reverse(?config(started_apps, Config))]. + +test_newentry(Config) -> + majority(fun test_newentry_/1, Config). + +test_newentry_({cleanup, Config}) -> + restart_exometer_core(); +test_newentry_(Config) -> + {ok, Info} = start_logger_and_reporter(test_udp, Config), + Tab = ets_tab(Info), + [] = ets:tab2list(Tab), + exometer:new([c], counter, []), + %% exometer_report:trigger_interval(test_udp, main), + R1 = check_logger_msg(), + [{_,{newentry,#exometer_entry{name = [c], type = counter}} = R1}] = + ets:tab2list(Tab), + ok. + +test_subscribe(Config) -> + majority(fun test_subscribe_/1, Config). + +test_subscribe_({cleanup, _Config}) -> + restart_exometer_core(); +test_subscribe_(Config) -> + ok = exometer:new([c], counter, []), + {ok, _Info} = start_logger_and_reporter(test_udp, Config), + exometer_report:subscribe(test_udp, [c], value, main, true), + %% exometer_report:trigger_interval(test_udp, main), + {subscribe, [{metric, [c]}, + {datapoint, value} | _]} = check_logger_msg(), + ok. + +test_subscribe_find(Config) -> + majority(fun test_subscribe_find_/1, Config). + +test_subscribe_find_({cleanup, _Config}) -> + restart_exometer_core(); +test_subscribe_find_(Config) -> + ok = exometer:new([c,1], counter, []), + ok = exometer:new([c,2], counter, []), + {ok, Info} = start_logger_and_reporter(test_udp, Config), + exometer_report:subscribe(test_udp, {find,[c,'_']}, value, main, true), + {subscribe, [{metric, {find,[c,'_']}}, + {datapoint, value} | _]} = R1 = check_logger_msg(), + exometer_report:trigger_interval(test_udp, main), + {report, [{prefix,[]},{metric,[c,1]}|_]} = R2 = check_logger_msg(), + {report, [{prefix,[]},{metric,[c,2]}|_]} = R3 = check_logger_msg(), + Tab = ets_tab(Info), + [{_,R1},{_,R2},{_,R3}] = ets:tab2list(Tab), + ok. + +test_subscribe_select(Config) -> + majority(fun test_subscribe_select_/1, Config). + +test_subscribe_select_({cleanup, _Config}) -> + restart_exometer_core(); +test_subscribe_select_(Config) -> + ok = exometer:new([c,1], counter, []), + ok = exometer:new([c,2], counter, []), + ok = exometer:new([c,3], counter, []), + {ok, Info} = start_logger_and_reporter(test_udp, Config), + exometer_report:subscribe( + test_udp, + {select,[{ {[c,'$1'],'_','_'},[{'<','$1',3}], ['$_'] }]}, + value, main, true), + {subscribe, [{metric, {select,_}}, + {datapoint, value} | _]} = R1 = check_logger_msg(), + exometer_report:trigger_interval(test_udp, main), + {report, [{prefix,[]},{metric,[c,1]}|_]} = R2 = check_logger_msg(), + {report, [{prefix,[]},{metric,[c,2]}|_]} = R3 = check_logger_msg(), + Tab = ets_tab(Info), + [{_,R1},{_,R2},{_,R3}] = ets:tab2list(Tab), + ok. + +test_logger_flow_control(Config) -> + test_subscribe_find([{input_port_options, [{active, false}]}|Config]). + +test_logger_flow_control_2(Config) -> + test_subscribe_find([{input_port_options, [{active, once}]}|Config]). + +start_logger_and_reporter(Reporter, Config) -> + Port = get_port(Config), + IPO = ensure_reuse(config(input_port_options, Config, [])), + ct:log("IPO = ~p~n", [IPO]), + Res = exometer_report_logger:new( + [{id, ?MODULE}, + {input, [{mode, plugin}, + {module, exometer_test_udp_reporter}, + {state, {Port, IPO}}]}, + {output, [{mode, plugin}, + {module, exometer_test_udp_reporter}]}, + {output, [{mode, ets}]}, + {output, [{mode, plugin}, + {module, ?MODULE}, + {state, self()}]}]), + ct:log("Logger start: ~p~n", [Res]), + Info = exometer_report_logger:info(), + ct:log("Logger Info = ~p~n", [Info]), + ok = exometer_report:add_reporter( + Reporter, + [{module, exometer_test_udp_reporter}, + {hostname, "localhost"}, + {port, Port}, + {intervals, [{main, manual}]}]), + {ok, Info}. + +ensure_reuse(Opts) -> + case lists:keyfind(reuseaddr, 1, Opts) of + {_, _} -> + Opts; + false -> + [{reuseaddr, true}|Opts] + end. + +check_logger_msg() -> + receive + {logger_got, Data} -> + ct:log("logger_got: ~p~n", [Data]), + Data + after 5000 -> + error(logger_ack_timeout) + end. + +ets_tab(Info) -> + [T] = [tree_opt([output,ets,tab], I) || {_,[{id,?MODULE}|I]} <- Info], + T. + +get_port(Config) -> + config(port, Config, ?DEFAULT_PORT). + +config(Key, Config, Default) -> + case ?config(Key, Config) of + undefined -> + Default; + Value -> + Value + end. + +tree_opt([H|T], L) when is_list(L) -> + case lists:keyfind(H, 1, L) of + {_, Val} -> + case T of + [] -> Val; + [_|_] -> + tree_opt(T, Val) + end; + false -> + undefined + end; +tree_opt([], _) -> + undefined. + + +logger_init_output(Pid) -> + {ok, Pid}. + +logger_handle_data(Data, Pid) -> + Pid ! {logger_got, Data}, + {Data, Pid}. + +restart_exometer_core() -> + application:stop(exometer_core), + application:start(exometer_core). diff --git a/test/exometer_test_udp_reporter.erl b/test/exometer_test_udp_reporter.erl new file mode 100644 index 0000000..9a65d5f --- /dev/null +++ b/test/exometer_test_udp_reporter.erl @@ -0,0 +1,158 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at http://mozilla.org/MPL/2.0/. +%% +%% ------------------------------------------------------------------- +-module(exometer_test_udp_reporter). + +-behaviour(exometer_report). +-export( + [ + exometer_init/1, + exometer_info/2, + exometer_cast/2, + exometer_call/3, + exometer_report/5, + exometer_subscribe/5, + exometer_unsubscribe/4, + exometer_newentry/2, + exometer_setopts/4, + exometer_terminate/2 + ]). + +-behaviour(exometer_report_logger). +-export([ + logger_init_input/1, + logger_init_output/1, + logger_handle_data/2 + ]). + +-import(exometer_util, [get_opt/2, get_opt/3]). + +-include_lib("kernel/include/inet.hrl"). +-record(st, {socket, address, port, type_map, prefix = []}). + +-define(DEFAULT_HOST, "localhost"). + +exometer_init(Opts) -> + Port = get_opt(port, Opts), + {ok, Host} = inet:gethostbyname(get_opt(hostname, Opts, ?DEFAULT_HOST)), + [Addr|_] = Host#hostent.h_addr_list, + AddrType = Host#hostent.h_addrtype, + TypeMap = get_opt(type_map, Opts, []), + Prefix = get_opt(prefix, Opts, []), + case gen_udp:open(0, [AddrType]) of + {ok, Socket} -> + {ok, #st{socket = Socket, address = Addr, port = Port, + type_map = TypeMap, prefix = Prefix}}; + {error, _} = Error -> + Error + end. + +exometer_report(Metric, DataPoint, Extra, Value, #st{type_map = TypeMap, + prefix = Pfx} = St) -> + RptType = exometer_util:report_type({Metric,DataPoint}, Extra, TypeMap), + ok = send({report, [{prefix, Pfx}, + {metric, Metric}, + {datapoint, DataPoint}, + {extra, Extra}, + {report_type, RptType}, + {value, Value}]}, St), + {ok, St}. + +exometer_subscribe(Metric, DataPoint, Extra, Interval, St) -> + ok = send({subscribe, [{metric, Metric}, + {datapoint, DataPoint}, + {extra, Extra}, + {interval, Interval}]}, St), + {ok, St}. + +exometer_unsubscribe(Metric, DataPoint, Extra, St) -> + ok = send({unsubscribe, [{metric, Metric}, + {datapoint, DataPoint}, + {extra, Extra}]}, St), + {ok, St}. + +exometer_call(Req, From, St) -> + ok = send({call, Req, From}, St), + {reply, {test_reply, ok}, St}. + +exometer_cast(Cast, St) -> + ok = send({cast, Cast}, St), + {ok, St}. + +exometer_info(I, St) -> + ok = send({info, I}, St), + {ok, St}. + +exometer_newentry(E, St) -> + ok = send({newentry, E}, St), + {ok, St}. + +exometer_setopts(Metric, Opts, Status, St) -> + ok = send({setopts, [{metric, Metric}, + {options, Opts}, + {status, Status}]}, St), + {ok, St}. + +exometer_terminate(_, _) -> + ok. + +send(Term, #st{socket = Socket, address = Address, port = Port}) -> + gen_udp:send(Socket, Address, Port, term_to_binary(Term, [compressed])). + + +logger_init_input({Port, Opts}) -> + Parent = self(), + {Pid,_} = spawn_monitor( + fun() -> + erlang:monitor(process, Parent), + {ok, Socket} = gen_udp:open(Port, [binary|Opts]), + check_active(Socket, Parent), + input_loop(Socket, Parent) + end), + {ok, Pid}. + +logger_init_output(_) -> + {ok, []}. + +logger_handle_data(Data, St) -> + {binary_to_term(iolist_to_binary([Data])), St}. + +input_loop(Socket, Parent) -> + case receive + {udp, Socket, _Host, _Port, Data} -> + Parent ! {plugin, self(), Data}; + {udp_passive, Socket} -> + Parent ! {plugin_passive, self()}, check; + {plugin_active, Active} -> + ct:log("{plugin_active, ~p}", [Active]), + inet:setopts(Socket, [{active, Active}]); + {'DOWN', _, process, Parent, _} -> + exit(normal) + after 5000 -> + io:fwrite(user, "input_loop timeout~n", []) + end of + check -> + check_active(Socket, Parent); + _ -> + ok + end, + input_loop(Socket, Parent). + +check_active(Socket, Parent) -> + case inet:getopts(Socket, [active]) of + {ok, [{active, A}]} -> + case A of + _ when A==0; A==false; A==once -> + Parent ! {plugin_passive, self()}; + _ -> + ok + end; + _ -> + ok + end. diff --git a/test/exometer_test_util.erl b/test/exometer_test_util.erl new file mode 100644 index 0000000..fdc57cb --- /dev/null +++ b/test/exometer_test_util.erl @@ -0,0 +1,80 @@ +-module(exometer_test_util). + +-export([ensure_all_started/1, + majority/2, + majority/3]). + +-define(MAJORITY_COUNT_ENV, "CT_MAJORITY_COUNT"). +-define(DEFAULT_MAJORITY_COUNT, 5). + +%% This implementation is originally from Basho's Webmachine. On +%% older versions of Erlang, we don't have +%% application:ensure_all_started, so we use this wrapper function to +%% either use the native implementation or our own version, depending +%% on what's available. +-spec ensure_all_started(atom()) -> {ok, [atom()]} | {error, term()}. +ensure_all_started(App) -> + case erlang:function_exported(application, ensure_all_started, 1) of + true -> + application:ensure_all_started(App); + false -> + ensure_all_started(App, []) + end. + +%% This implementation is originally from Basho's +%% Webmachine. Reimplementation of ensure_all_started. NOTE this does +%% not behave the same as the native version in all cases, but as a +%% quick hack it works well enough for our purposes. Eventually I +%% assume we'll drop support for older versions of Erlang and this can +%% be eliminated. +ensure_all_started(App, Apps0) -> + case application:start(App) of + ok -> + {ok, lists:reverse([App | Apps0])}; + {error,{already_started,App}} -> + {ok, lists:reverse(Apps0)}; + {error,{not_started,BaseApp}} -> + {ok, Apps} = ensure_all_started(BaseApp, Apps0), + ensure_all_started(App, [BaseApp|Apps]) + end. + +majority(F, Cfg) -> + case os:getenv(?MAJORITY_COUNT_ENV) of + false -> + majority(?DEFAULT_MAJORITY_COUNT, F, Cfg); + Count -> + case catch erlang:list_to_integer(Count) of + C when is_integer(C) -> + majority(C, F, Cfg); + _ -> + ct:pal("Invalid value for '~s' given", [?MAJORITY_COUNT_ENV]), + majority(?DEFAULT_MAJORITY_COUNT, F, Cfg) + end + end. + +%% Run test N times. Success if a majority of the tests succeed. +%% Cleanup between runs done by calling F({cleanup, Config}) +%% Returns 'ok' or {error, Info}. +%% +majority(N, F, Cfg) -> + majority(N, F, Cfg, []). + +majority(0, _, _, Hist) -> + Failed = length([1 || {caught,_,_} <- Hist]), + LogMsg = lists:flatten(io_lib:format("majority: Failed = ~p, Hist = ~p", [Failed, Hist])), + ct:pal(LogMsg), + case {Failed, length(Hist)} of + {Lf, L} when Lf >= L div 2 -> + ct:fail({error, {too_many_failures, Hist}}); + _ -> + {comment, LogMsg} + end; +majority(N, F, Cfg, Hist) when N > 0 -> + Res = try F(Cfg) + catch + C:R -> + {caught, C, R} + after + F({cleanup, Cfg}) + end, + majority(N-1, F, Cfg, [Res|Hist]).