diff --git a/rebar.config b/rebar.config index 9c18ccf..e85ccb2 100644 --- a/rebar.config +++ b/rebar.config @@ -19,6 +19,8 @@ {xref_checks,[undefined_function_calls, undefined_functions, locals_not_used, deprecated_function_calls, deprecated_functions]}. +{plugins, [rebar3_ex_doc]}. + {hex, [ {doc, #{provider => ex_doc}} ]}. diff --git a/src/telemetry.erl b/src/telemetry.erl index 20339fd..d81239c 100644 --- a/src/telemetry.erl +++ b/src/telemetry.erl @@ -1,11 +1,3 @@ -%%%------------------------------------------------------------------- -%% @doc `telemetry' allows you to invoke certain functions whenever a -%% particular event is emitted. -%% -%% For more information see the documentation for {@link attach/4}, {@link attach_many/4} -%% and {@link execute/2}. -%% @end -%%%------------------------------------------------------------------- -module(telemetry). -export([attach/4, @@ -20,6 +12,14 @@ -include("telemetry.hrl"). +?MODULEDOC(""" +`telemetry` allows you to invoke certain functions whenever a +particular event is emitted. + +For more information see the documentation for `attach/4`, `attach_many/4` +and `execute/2`. +"""). + -type handler_id() :: term(). -type event_name() :: [atom(), ...]. -type event_measurements() :: map(). @@ -49,27 +49,31 @@ -import_lib("kernel/import/logger.hrl"). -%% @doc Attaches the handler to the event. -%% -%% `handler_id' must be unique, if another handler with the same ID already exists the -%% `{error, already_exists}' tuple is returned. -%% -%% See {@link execute/3} to learn how the handlers are invoked. -%% -%% Note: due to how anonymous functions are implemented in the Erlang VM, it is best to use -%% function captures (i.e. `fun mod:fun/4' in Erlang or `&Mod.fun/4' in Elixir) as event handlers -%% to achieve maximum performance. In other words, avoid using literal anonymous functions -%% (`fun(...) -> ... end' or `fn ... -> ... end') or local function captures (`fun handle_event/4' -%% or `&handle_event/4' ) as event handlers. -%% -%% All the handlers are executed by the process dispatching event. If the function fails (raises, -%% exits or throws) then the handler is removed and a failure event is emitted. -%% -%% Handler failure events `[telemetry, handler, failure]' should only be used for monitoring -%% and diagnostic purposes. Re-attaching a failed handler will likely result in the handler -%% failing again. -%% -%% Note that you should not rely on the order in which handlers are invoked. +?DOC(""" +Attaches the handler to the event. + +`handler_id` must be unique, if another handler with the same ID already exists the +`{error, already_exists}` tuple is returned. + +See `execute/3` to learn how the handlers are invoked. + +> #### Function Captures {: .info} +> +> Due to how anonymous functions are implemented in the Erlang VM, it is best to use +> function captures (`fun mod:fun/4` in Erlang or `&Mod.fun/4` in Elixir) as event handlers +> to achieve the best performance. In other words, avoid using literal anonymous functions +> (`fun(...) -> ... end` or `fn ... -> ... end`) or local function captures (`fun handle_event/4` +> or `&handle_event/4`) as event handlers. + +All the handlers are executed by the process dispatching event. If the function fails (raises, +exits or throws) then the handler is removed and a failure event is emitted. + +Handler failure events `[telemetry, handler, failure]` should only be used for monitoring +and diagnostic purposes. Re-attaching a failed handler will likely result in the handler +failing again. + +Note that you should not rely on the order in which handlers are invoked. +"""). -spec attach(HandlerId, EventName, Function, Config) -> ok | {error, already_exists} when HandlerId :: handler_id(), EventName :: event_name(), @@ -78,26 +82,28 @@ attach(HandlerId, EventName, Function, Config) -> attach_many(HandlerId, [EventName], Function, Config). -%% @doc Attaches the handler to many events. -%% -%% The handler will be invoked whenever any of the events in the `event_names' list is emitted. Note -%% that failure of the handler on any of these invocations will detach it from all the events in -%% `event_name' (the same applies to manual detaching using {@link detach/1}). -%% -%% Note: due to how anonymous functions are implemented in the Erlang VM, it is best to use -%% function captures (i.e. `fun mod:fun/4' in Erlang or `&Mod.fun/4' in Elixir) as event handlers -%% to achieve maximum performance. In other words, avoid using literal anonymous functions -%% (`fun(...) -> ... end' or `fn ... -> ... end') or local function captures (`fun handle_event/4' -%% or `&handle_event/4' ) as event handlers. -%% -%% All the handlers are executed by the process dispatching event. If the function fails (raises, -%% exits or throws) a handler failure event is emitted and then the handler is removed. -%% -%% Handler failure events `[telemetry, handler, failure]' should only be used for monitoring -%% and diagnostic purposes. Re-attaching a failed handler will likely result in the handler -%% failing again. -%% -%% Note that you should not rely on the order in which handlers are invoked. +?DOC(""" +Attaches the handler to many events. + +The handler will be invoked whenever any of the events in the `event_names` list is emitted. Note +that failure of the handler on any of these invocations will detach it from all the events in +`event_name` (the same applies to manual detaching using `detach/1`). + +Note: due to how anonymous functions are implemented in the Erlang VM, it is best to use +function captures (i.e. `fun mod:fun/4` in Erlang or `&Mod.fun/4` in Elixir) as event handlers +to achieve maximum performance. In other words, avoid using literal anonymous functions +(`fun(...) -> ... end` or `fn ... -> ... end`) or local function captures (`fun handle_event/4` +or `&handle_event/4`) as event handlers. + +All the handlers are executed by the process dispatching event. If the function fails (raises, +exits or throws) a handler failure event is emitted and then the handler is removed. + +Handler failure events `[telemetry, handler, failure]` should only be used for monitoring +and diagnostic purposes. Re-attaching a failed handler will likely result in the handler +failing again. + +Note that you should not rely on the order in which handlers are invoked. +"""). -spec attach_many(HandlerId, [EventName], Function, Config) -> ok | {error, already_exists} when HandlerId :: handler_id(), EventName :: event_name(), @@ -118,30 +124,31 @@ attach_many(HandlerId, EventNames, Function, Config) when is_function(Function, end, telemetry_handler_table:insert(HandlerId, EventNames, Function, Config). -%% @doc Removes the existing handler. -%% -%% If the handler with given ID doesn't exist, `{error, not_found}' is returned. +?DOC(""" +Removes the existing handler. + +If the handler with given ID doesn't exist, `{error, not_found}` is returned. +"""). -spec detach(handler_id()) -> ok | {error, not_found}. detach(HandlerId) -> telemetry_handler_table:delete(HandlerId). -%% @doc Emits the event, invoking handlers attached to it. -%% -%% When the event is emitted, the handler function provided to {@link attach/4} is called with four -%% arguments: -%% -%% -%%

Best practices and conventions:

-%% -%%

-%% While you are able to emit messages of any `event_name' structure, it is recommended that you follow the -%% the guidelines laid out in {@link span/3} if you are capturing start/stop events. -%%

+?DOC(""" +Emits the event, invoking handlers attached to it. + +When the event is emitted, the handler function provided to `attach/4` is called with four +arguments: + + * the event name + * the map of measurements + * the map of event metadata + * the handler configuration given to `attach/4` + +#### Best practices and conventions: + +While you are able to emit messages of any `event_name` structure, it is recommended that you follow the +the guidelines laid out in `span/3` if you are capturing start/stop events. +"""). -spec execute(EventName, Measurements, Metadata) -> ok when EventName :: event_name(), Measurements :: event_measurements() | event_value(), @@ -176,143 +183,134 @@ execute([_ | _] = EventName, Measurements, Metadata) when is_map(Measurements) a end, lists:foreach(ApplyFun, Handlers). -%% @doc Runs the provided `SpanFunction', emitting start and stop/exception events, invoking the handlers attached to each. -%% -%% The `SpanFunction' must return a `{result, stop_metadata}' or a `{result, extra_measurements, stop_metadata}` tuple. -%% -%% When this function is called, 2 events will be emitted via {@link execute/3}. Those events will be one of the following -%% pairs: -%% -%% -%% However, note that in case the current process crashes due to an exit signal -%% of another process, then none or only part of those events would be emitted. -%% Below is a breakdown of the measurements and metadata associated with each individual event. -%% -%% When providing `StartMetadata' and `StopMetadata', these values will be sent independently to `start' and -%% `stop' events. If an exception occurs, exception metadata will be merged onto the `StartMetadata'. In general, -%% it is highly recommended that `StopMetadata' should include the values from `StartMetadata' -%% so that handlers, such as those used for metrics, can rely entirely on the `stop' event. Failure to include -%% all of `StartMetadata' in `StopMetadata' can add significant complexity to event handlers. -%% -%% A default span context is added to event metadata under the `telemetry_span_context' key if none is provided by -%% the user in the `StartMetadata'. This context is useful for tracing libraries to identify unique -%% executions of span events within a process to match start, stop, and exception events. Metadata keys, which -%% should be available to both `start' and `stop' events need to supplied separately for `StartMetadata' and -%% `StopMetadata'. -%% -%% If `SpanFunction` returns `{result, extra_measurements, stop_metadata}`, then a map of extra measurements -%% will be merged with the measurements automatically provided. This is useful if you want to return, for example, -%% bytes from an HTTP request. The standard measurements `duration` and `monotonic_time` cannot be overridden. -%% -%% For `telemetry' events denoting the start of a larger event, the following data is provided: -%% -%%

-%%

-%%

-%% -%% For `telemetry' events denoting the stop of a larger event, the following data is provided: -%%

-%%

-%%

-%% -%% For `telemetry' events denoting an exception of a larger event, the following data is provided: -%%

-%%

-%%

+?DOC(""" +Runs the provided `SpanFunction`, emitting start and stop/exception events, invoking the handlers attached to each. + +The `SpanFunction` must return a `{result, stop_metadata}` or a `{result, extra_measurements, stop_metadata}` tuple. + +When this function is called, 2 events will be emitted via `execute/3`. Those events will be one of the following +pairs: + + * `EventPrefix ++ [start]` and `EventPrefix ++ [stop]` + * `EventPrefix ++ [start]` and `EventPrefix ++ [exception]` + +However, note that in case the current process crashes due to an exit signal +of another process, then none or only part of those events would be emitted. +Below is a breakdown of the measurements and metadata associated with each individual event. + +When providing `StartMetadata` and `StopMetadata`, these values will be sent independently to `start` and +`stop` events. If an exception occurs, exception metadata will be merged onto the `StartMetadata`. In general, +it is **highly recommended** that `StopMetadata` should include the values from `StartMetadata` +so that handlers, such as those used for metrics, can rely entirely on the `stop` event. Failure to include +all of `StartMetadata` in `StopMetadata` can add significant complexity to event handlers. + +A default span context is added to event metadata under the `telemetry_span_context` key if this key is not provided +by the user in the `StartMetadata`. This context is useful for tracing libraries to identify unique +executions of span events within a process to match start, stop, and exception events. Metadata keys which +should be available to both `start` and `stop` events need to supplied separately for `StartMetadata` and +`StopMetadata`. + +If `SpanFunction` returns `{result, extra_measurements, stop_metadata}`, then a map of extra measurements +will be merged with the measurements automatically provided. This is useful if you want to return, for example, +bytes from an HTTP request. The standard measurements `duration` and `monotonic_time` cannot be overridden. + +For `telemetry` events denoting the **start** of a larger event, the following data is provided: + + * Event: + + ``` + EventPrefix ++ [start] + ``` + + * Measurements: + + ``` + #{ + % The current system time in native units from + % calling: erlang:system_time() + system_time => integer(), + monotonic_time => integer(), + } + ``` + + * Metadata: + + ``` + #{ + telemetry_span_context => term(), + % User defined metadata as provided in StartMetadata + ... + } + ``` + + + +For `telemetry` events denoting the **stop** of a larger event, the following data is provided: + + * Event: + + ``` + EventPrefix ++ [stop] + ``` + + * Measurements: + + ``` + #{ + % The current monotonic time minus the start monotonic time in native units + % by calling: erlang:monotonic_time() - start_monotonic_time + duration => integer(), + monotonic_time => integer(), + % User defined measurements when returning `SpanFunction` as a 3 element tuple + } + ``` + + * Metadata: + + ``` + #{ + % An optional error field if the stop event is the result of an error + % but not necessarily an exception. + error => term(), + telemetry_span_context => term(), + % User defined metadata as provided in StopMetadata + ... + } + ``` + +For `telemetry` events denoting an **exception** of a larger event, the following data is provided: + + * Event: + + ``` + EventPrefix ++ [exception] + ``` + + * Measurements: + + ``` + #{ + % The current monotonic time minus the start monotonic time in native units + % by calling: erlang:monotonic_time() - start_monotonic_time + duration => integer(), + monotonic_time => integer() + } + ``` + + * Metadata: + + ``` + #{ + kind => throw | error | exit, + reason => term(), + stacktrace => list(), + telemetry_span_context => term(), + % User defined metadata as provided in StartMetadata + ... + } + ``` + +"""). -spec span(event_prefix(), event_metadata(), span_function()) -> span_result(). span(EventPrefix, StartMetadata, SpanFunction) -> StartTime = erlang:monotonic_time(), @@ -353,18 +351,22 @@ span(EventPrefix, StartMetadata, SpanFunction) -> erlang:raise(Class, Reason, Stacktrace) end. -%% @equiv execute(EventName, Measurements, #{}) +?DOC(""" +Same as [`execute(EventName, Measurements, #{})`](`execute/3`). +"""). -spec execute(EventName, Measurements) -> ok when EventName :: event_name(), Measurements :: event_measurements() | event_value(). execute(EventName, Measurements) -> execute(EventName, Measurements, #{}). -%% @doc Returns all handlers attached to events with given prefix. -%% -%% Handlers attached to many events at once using {@link attach_many/4} will be listed once for each -%% event they're attached to. -%% Note that you can list all handlers by feeding this function an empty list. +?DOC(""" +Returns all handlers attached to events with given prefix. + +Handlers attached to many events at once using `attach_many/4` will be listed once for each +event they're attached to. +Note that you can list all handlers by feeding this function an empty list. +"""). -spec list_handlers(event_prefix()) -> [handler()]. list_handlers(EventPrefix) -> assert_event_prefix(EventPrefix), @@ -410,7 +412,7 @@ assert_event_name(Term) -> merge_ctx(#{telemetry_span_context := _} = Metadata, _Ctx) -> Metadata; merge_ctx(Metadata, Ctx) -> Metadata#{telemetry_span_context => Ctx}. -%% @private +?DOC(false). report_cb(#{handler_id := Id}) -> {"The function passed as a handler with ID ~w is a local function.\n" "This means that it is either an anonymous function or a capture of a function " diff --git a/src/telemetry.hrl b/src/telemetry.hrl index f1a8aaa..e8eaa2b 100644 --- a/src/telemetry.hrl +++ b/src/telemetry.hrl @@ -20,3 +20,11 @@ -else. -define(LOG_WARNING(Msg, Args), error_logger:warning_msg(Msg, Args)). -endif. + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. \ No newline at end of file diff --git a/src/telemetry_test.erl b/src/telemetry_test.erl index 61a4317..c813608 100644 --- a/src/telemetry_test.erl +++ b/src/telemetry_test.erl @@ -1,51 +1,77 @@ -%%%------------------------------------------------------------------- -%% @doc Functions for testing execution of Telemetry events. -%% -%% Testing that the correct Telemetry events are emitted with the -%% right measurements and metadata is essential for library authors. -%% It helps to maintain stable APIs and avoid accidental changes -%% to events. -%% @end -%%%------------------------------------------------------------------- - -module(telemetry_test). +-include("telemetry.hrl"). + +?MODULEDOC(""" +Functions for testing execution of Telemetry events. + +Testing that the correct Telemetry events are emitted with the +right measurements and metadata is essential for library authors. +It helps to maintain stable APIs and avoid accidental changes +to events. +"""). + -export([attach_event_handlers/2, handle_event/4]). -%% @doc Attaches a "message" handler to the given events. -%% -%% The attached handler sends a message to `destination_pid' every time it handles one of the -%% events in `events'. The function returns a reference that you can use to make sure that -%% messages come from this handler. This reference is also used as the handler ID, so you -%% can use it to detach the handler with {@link telemetry:detach/1}. -%% -%% The shape of messages sent to `destination_pid' is: -%% -%% ``` -%% {Event, Ref, Measurements, Metadata} -%% ''' -%% -%% For example, in Erlang a test could look like this: -%% -%% ``` -%% Ref = telemetry_test:attach_event_handlers(self(), [[some, event]]), -%% function_that_emits_the_event(), -%% receive -%% {[some, event], Ref, #{measurement := _}, #{meta := _}} -> -%% telemetry:detach(Ref) -%% after 1000 -> -%% ct:fail(timeout_receive_attach_event_handlers) -%% end. -%% ''' -%% -%% In Elixir, a similar test would look like this: -%% -%% ``` -%% ref = :telemetry_test.attach_event_handlers(self(), [[:some, :event]]) -%% function_that_emits_the_event() -%% assert_received {[:some, :event], ^ref, %{measurement: _}, %{meta: _}} -%% ''' -%% +?DOC(""" +Attaches a "message" handler to the given events. + +The attached handler sends a message to `DestinationPID` every time it handles one of the +events in `events`. The function returns a reference that you can use to make sure that +messages come from this handler. This reference is also used as the handler ID, so you +can use it to detach the handler with `telemetry:detach/1`. + +The shape of messages sent to `DestinationPID` is: + + + +### Erlang + +```erlang +{Event, Ref, Measurements, Metadata} +``` + +### Elixir + +```elixir +{event, ref, measurements, metadata} +``` + + + +## Examples + + + +### Erlang + +An example of a test in Erlang (using [`ct`](https://www.erlang.org/docs/23/man/ct)) could +look like this: + +```erlang +Ref = telemetry_test:attach_event_handlers(self(), [[some, event]]), +function_that_emits_the_event(), +receive + {[some, event], Ref, #{measurement := _}, #{meta := _}} -> + telemetry:detach(Ref) +after 1000 -> + ct:fail(timeout_receive_attach_event_handlers) +end. +``` + +### Elixir + +An example of an ExUnit test in Elixir could look like this: + +```elixir +ref = :telemetry_test.attach_event_handlers(self(), [[:some, :event]]) +function_that_emits_the_event() +assert_received {[:some, :event], ^ref, %{measurement: _}, %{meta: _}} +``` + + + +"""). -spec attach_event_handlers(DestinationPID, Events) -> reference() when DestinationPID :: pid(), Events :: [telemetry:event_name(), ...]. @@ -55,6 +81,6 @@ attach_event_handlers(DestPID, Events) when is_pid(DestPID) and is_list(Events) telemetry:attach_many(Ref, Events, fun telemetry_test:handle_event/4, Config), Ref. -%% @hidden +?DOC(false). handle_event(Event, Measurements, Metadata, #{dest_pid := DestPID, ref := Ref}) -> DestPID ! {Event, Ref, Measurements, Metadata}.