This commit is contained in:
2026-07-14 22:50:46 +04:00
parent 27143319e3
commit bd19e0682b
3116 changed files with 467189 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,133 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.3.0](https://github.com/beam-telemetry/telemetry_poller/tree/v1.3.0)
### Added
- Add `atom_limit`, `process_limit`, and `port_limit` measurements to the `[vm, system_counts]` event. (#79)
## [1.2.0](https://github.com/beam-telemetry/telemetry_poller/tree/v1.2.0)
### Added
- Support `persistent_term` measurements.
- Require Erlang/OTP 24+.
## [1.1.0](https://github.com/beam-telemetry/telemetry_poller/tree/v1.1.0)
### Added
- Add the `init_delay` configuration option. (#70)
## [1.0.0](https://github.com/beam-telemetry/telemetry_poller/tree/v1.0.0)
This release marks stability of the API. The library now requires Telemetry ~> 1.0.
### Fixed
- Fix disabling the default poller via application environment.
### Changed
- Drop support for OTP 20.
## [0.5.1](https://github.com/beam-telemetry/telemetry_poller/tree/v0.5.1)
### Added
- Support `{:global, atom()}` and `{:via, module(), term()}` names for the poller process.
## [0.5.0](https://github.com/beam-telemetry/telemetry_poller/tree/v0.5.0)
### Added
- `system_counts` measurement
## [0.4.1](https://github.com/beam-telemetry/telemetry_poller/tree/v0.4.1)
### Changed
- Improve docs
- No longer add a default name to telemetry processes
## [0.4.0](https://github.com/beam-telemetry/telemetry_poller/tree/v0.4.0)
Telemetry Poller has been rewritten in Erlang so it can be used by the overall Erlang community.
Therefore, the `Telemetry.Poller` module must now be accessed as `telemetry_poller`.
A new `process_info` measurement has also been added and the `vm_measurements` and `measurements`
keys have been merged into a single `measurements` key for simplicity.
### Added
- `:process_info` measurement
### Changed
- `vm_measurements` and `measurements` have been merged into `measurements`
- `Telemetry.Poller` has been rewritten to `telemetry_poller`
## [0.3.0](https://github.com/beam-telemetry/telemetry_poller/tree/v0.3.0)
This release marks the upgrade to Telemetry 0.4.0. This means that Poller measurements can emit a map
of values now instead of a single one, making it less "noisy" when it comes to number of emitted events.
All specific memory measurements have been replaced with a single `:memory` measurement sending all
the values that were emitted by old measurements at once.
`:run_queue_lengths` VM measurement has been removed for now, as we believe that as detailed data
as it provided is not necessary to effectively debug the system. `:total_run_queue_lengths` VM
measurement has been changed so that it reports a `:total` length of run queues, length of `:cpu`
run queues (including dirty CPU run queue), and length of (dirty) `:io` run queue.
### Added
- `:memory` VM measurement reporting all the data returned by `:erlang.memory/0` call.
### Changed
- `:total_run_queue_lengths` VM measurement is reporting a `:total`, `:cpu` and `:io` run queue lengths
now. See documentation for more details.
### Removed
- `:total_memory`, `:atom_memory`, `:atom_used_memory`, `:processes_memory`, `:processes_used_memory`,
`:binary_memory`, `:ets_memory`, `:code_memory` and `:system_memory` VM measurements have been removed.
Please use the `:memory` measurement now instead.
## [0.2.0](https://github.com/beam-telemetry/telemetry_poller/tree/v0.2.0)
### Added
- Added `:total_run_queue_lengths` and `:run_queue_lengths` memory measurements;
### Changed
- `:total_run_queue_lengths` is now included in the set of default VM measurements;
- A default Poller process, with a default set of VM measurements, is started when `:telemetry_poller`
application starts (configurable via `:telemetry.poller, :default` application environment).
- VM measurements are now provided to Poller's `:vm_measurements` option. Passing atom `:default`
to this option makes the Poller use a default set of VM measurements;
- Telemetry has been upgraded to version 0.3, meaning that VM measurements now use this version to
emit the events.
### Removed
- `Telemetry.Poller.vm_measurements/0` function has been removed in favor of `:vm_measurements`
option.
### Fixed
- Fixed the type definition of `Telemetry.Poller.measurement/0` type - no Dialyzer warnings are
emitted when running it on the project.
## [0.1.0](https://github.com/beam-telemetry/telemetry_poller/tree/v0.1.0)
### Added
- The Poller process periodically invoking registered functions which emit Telemetry events.
It supports VM memory measurements and custom measurements provided as MFAs.

View File

@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -0,0 +1,13 @@
Copyright (c) 2018, Chris McCord and Erlang Solutions
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,122 @@
# telemetry_poller
[![Test](https://github.com/beam-telemetry/telemetry_poller/actions/workflows/test.yml/badge.svg)](https://github.com/beam-telemetry/telemetry_poller/actions/workflows/test.yml)
[![Codecov](https://codecov.io/gh/beam-telemetry/telemetry_poller/branch/master/graphs/badge.svg)](https://codecov.io/gh/beam-telemetry/telemetry_poller/branch/master/graphs/badge.svg)
Allows to periodically collect measurements and dispatch them as Telemetry events.
`telemetry_poller` by default runs a poller to perform VM measurements:
* `[vm, memory]` - contains the total memory, process memory, and all other keys in `erlang:memory/0`
* `[vm, total_run_queue_lengths]` - returns the run queue lengths for CPU and IO schedulers. It contains the `total`, `cpu` and `io` measurements
* `[vm, system_counts]` - returns the current process, atom and port count as well as their respective limits as per `erlang:system_info/1`
* `[vm, persistent_term]` - number of terms and memory byte size for `persistent_term`
You can directly consume those events after adding `telemetry_poller` as a dependency.
Poller also provides a convenient API for running custom pollers.
## Defining custom measurements
Poller also includes conveniences for performing process-based measurements as well as custom ones.
### Erlang
First define the poller with the custom measurements. The first measurement is the built-in `process_info` measurement and the second one is given by a custom module-function-args defined by you:
```erlang
telemetry_poller:start_link(
[{measurements, [
{process_info, [{name, my_app_worker}, {event, [my_app, worker]}, {keys, [memory, message_queue_len]}]},
{example_app_measurements, dispatch_session_count, []}
]},
{period, timer:seconds(10)}, % configure sampling period - default is timer:seconds(5)
{init_delay, timer:seconds(600)}, % configure sampling initial delay - default is 0
{name, my_app_poller}
]).
```
Now define the custom measurement and you are good to go:
```erlang
-module(example_app_measurements).
dispatch_session_count() ->
% emit a telemetry event when called
telemetry:execute([example_app, session_count], #{count => example_app:session_count()}, #{}).
```
### Elixir
You typically start the poller as a child in your supervision tree:
```elixir
children = [
{:telemetry_poller,
# include custom measurement as an MFA tuple
measurements: [
{:process_info, name: :my_app_worker, event: [:my_app, :worker], keys: [:memory, :message_queue_len]},
{ExampleApp.Measurements, :dispatch_session_count, []},
],
period: :timer.seconds(10), # configure sampling period - default is :timer.seconds(5)
init_delay: :timer.seconds(600), # configure sampling initial delay - default is 0
name: :my_app_poller}
]
Supervisor.start_link(children, strategy: :one_for_one)
```
The poller above has two periodic measurements. The first is the built-in `process_info` measurement that will gather the memory and message queue length of a process. The second is given by a custom module-function-args defined by you, such as below:
```elixir
defmodule ExampleApp.Measurements do
def dispatch_session_count() do
# emit a telemetry event when called
:telemetry.execute([:example_app, :session_count], %{count: ExampleApp.session_count()}, %{})
end
end
```
## Documentation
See [documentation](https://hexdocs.pm/telemetry_poller/) for more concrete examples and usage
instructions.
## VM metrics example
### Erlang
Find, in `examples/telemetry_poller_vm.erl`, an example on how to retrieve to VM measurements,
mentioned above.
To see it in action, fire up `rebar3 shell`, then
```erlang
{ok, telemetry_poller_vm} = c("examples/telemetry_poller_vm").
ok = file:delete("telemetry_poller_vm.beam"). % Deletes generated BEAM
ok = telemetry_poller_vm:attach().
```
### Elixir
Find, in `examples/TelemetryPollerVM.ex`, an example on how to retrieve to VM measurements,
mentioned above.
To see it in action, first compile the Erlang sources with `rebar3 compile`.
Then fire up `iex -pa "_build/default/lib/*/ebin"`, then
```elixir
{:ok, _} = Application.ensure_all_started(:telemetry_poller)
[TelemetryPollerVM] = c("examples/TelemetryPollerVM.ex")
:ok = TelemetryPollerVM.attach()
```
## Copyright and License
Copyright (c) 2025 Erlang Ecosystem Foundation.
telemetry_poller source code is released under Apache License, Version 2.0.
See [LICENSE](LICENSE) and [NOTICE](NOTICE) files for more information.

View File

@@ -0,0 +1,20 @@
{<<"app">>,<<"telemetry_poller">>}.
{<<"build_tools">>,[<<"rebar3">>]}.
{<<"description">>,
<<"Periodically collect measurements and dispatch them as Telemetry events.">>}.
{<<"files">>,
[<<"CHANGELOG.md">>,<<"LICENSE">>,<<"NOTICE">>,<<"README.md">>,
<<"rebar.config">>,<<"rebar.lock">>,<<"src">>,
<<"src/telemetry_poller.app.src">>,<<"src/telemetry_poller.erl">>,
<<"src/telemetry_poller_app.erl">>,<<"src/telemetry_poller_builtin.erl">>,
<<"src/telemetry_poller_sup.erl">>]}.
{<<"licenses">>,[<<"Apache-2.0">>]}.
{<<"links">>,
[{<<"GitHub">>,<<"https://github.com/beam-telemetry/telemetry_poller">>}]}.
{<<"name">>,<<"telemetry_poller">>}.
{<<"requirements">>,
[{<<"telemetry">>,
[{<<"app">>,<<"telemetry">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 1.0">>}]}]}.
{<<"version">>,<<"1.3.0">>}.

View File

@@ -0,0 +1,33 @@
{minimum_otp_vsn, "24.0"}.
{erl_opts, [debug_info]}.
{deps, [
{telemetry, "~> 1.0"}
]}.
{profiles, [
{test, [
{erl_opts, [nowarn_export_all]},
%% create junit xml for circleci
{ct_opts, [{ct_hooks, [cth_surefire]}]},
{src_dirs, ["src", "test/support"]}
]}
]}.
{shell, [{apps, [telemetry_poller]}]}.
{project_plugins, [rebar3_ex_doc]}.
{ex_doc, [
{main, "README"},
{extras, [<<"README.md">>, <<"CHANGELOG.md">>, <<"LICENSE">>, <<"NOTICE">>]},
{source_url, <<"https://github.com/beam-telemetry/telemetry_poller">>},
{source_ref, <<"v1.3.0">>}
]}.
{hex, [
{doc, #{provider => ex_doc}}
]}.
%% take out warnings for unused exported functions
{xref_checks,[undefined_function_calls, undefined_functions, locals_not_used,
deprecated_function_calls, deprecated_functions]}.

View File

@@ -0,0 +1,8 @@
{"1.2.0",
[{<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.0.0">>},0}]}.
[
{pkg_hash,[
{<<"telemetry">>, <<"0F453A102CDF13D506B7C0AB158324C337C41F1CC7548F0BC0E130BBF0AE9452">>}]},
{pkg_hash_ext,[
{<<"telemetry">>, <<"73BC09FA59B4A0284EFB4624335583C528E07EC9AE76ACA96EA0673850AEC57A">>}]}
].

View File

@@ -0,0 +1,12 @@
{application,telemetry_poller,
[{description,"Periodically collect measurements and dispatch them as Telemetry events."},
{vsn,"1.3.0"},
{registered,[]},
{mod,{telemetry_poller_app,[]}},
{applications,[kernel,stdlib,telemetry]},
{env,[]},
{modules,[]},
{licenses,["Apache-2.0"]},
{doc,"doc"},
{links,[{"GitHub",
"https://github.com/beam-telemetry/telemetry_poller"}]}]}.

View File

@@ -0,0 +1,505 @@
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-module(telemetry_poller).
?MODULEDOC("""
A time-based poller to periodically dispatch Telemetry events.
A poller is a process start in your supervision tree with a list
of measurements to perform periodically. On start it expects the
period in milliseconds and a list of measurements to perform. Initial delay
is an optional parameter that sets time delay in milliseconds before starting
measurements:
<!-- tabs-open -->
### Erlang
```
telemetry_poller:start_link([
{measurements, Measurements},
{period, Period},
{init_delay, InitDelay}
])
```
### Elixir
```
:telemetry_poller.start_link(
measurements: measurements,
period: period,
init_delay: init_delay
)
```
<!-- tabs-close -->
## Measurements
The following measurements are supported:
* `memory` (default)
* `total_run_queue_lengths` (default)
* `system_counts` (default)
* `persistent_term` (default)
* `{process_info, Proplist}`
* `{Module, Function, Args}`
We will discuss each measurement in detail. Also note that the
`telemetry_poller` application ships with a built-in poller that
measures `memory`, `total_run_queue_lengths`, `system_counts`, and `persistent_term`. This takes
the VM measurement out of the way so your application can focus
on what is specific to its behaviour.
### Memory
An event emitted as `[vm, memory]`. The measurement includes all
the key-value pairs returned by the `erlang:memory/0` function,
e.g. `total` for total memory, `processes_used` for memory used by
all processes, and so on.
### Total run queue lengths
On startup, the Erlang VM starts many schedulers to do both IO and
CPU work. If a process needs to do some work or wait on IO, it is
allocated to the appropriate scheduler. The measurement includes the
following keys:
* `total` - all schedulers (CPU + IO)
* `cpu` - CPU schedulers
* `io` - IO schedulers
### System counts
The measurement includes:
* `process_count` - the number of processes currently existing at the local node
* `atom_count` - the number of atoms currently existing at the local node
* `port_count` - the number of ports currently existing at the local node
* `process_limit` - the maximum number of processes allowed at the local node
* `atom_limit` - the maximum number of atoms allowed at the local node
* `port_limit` - the maximum number of ports allowed at the local node
### Persistent term (since 1.2.0)
An event emitted as `[vm, persistent_term]`. The measurement includes information
about persistent terms in the system, as returned by `persistent_term:info/0`:
* `count` - The number of persistent terms
* `memory` - The total amount of memory (measured in bytes) used by all persistent terms
### Process info
A measurement with information about a given process. It must be specified
alongside a proplist with the process name, the event name, and a list of
keys to be included:
<!-- tabs-open -->
### Erlang
```erlang
{process_info, [
{name, my_app_worker},
{event, [my_app, worker]},
{keys, [message_queue_len, memory]}
]}
```
### Elixir
```elixir
{:process_info, [
name: my_app_worker,
event: [my_app, worker],
keys: [message_queue_len, memory]
]}
```
<!-- tabs-close -->
### Custom measurements
Telemetry poller also allows you to perform custom measurements by passing
a module-function-args tuple:
<!-- tabs-open -->
### Erlang
```erlang
{my_app_example, measure, []}
```
### Elixir
```elixir
{MyApp.Example, :measure, []}
```
<!-- tabs-close -->
The given function will be invoked periodically and they must explicitly invoke the
`telemetry:execute/3` function. If the invocation of the MFA fails,
the measurement is removed from the Poller.
For all options, see `start_link/1`. The options listed there can be given
to the default poller as well as to custom pollers.
### Default poller
A default poller is started with `telemetry_poller` responsible for emitting
measurements for `memory` and `total_run_queue_lengths`. You can customize
the behaviour of the default poller by setting the `default` key under the
`telemetry_poller` application environment. Setting it to `false` disables
the poller.
## Examples
### Example 1: tracking number of active sessions in web application
Let's imagine that you have a web application and you would like to periodically
measure number of active user sessions.
<!-- tabs-open -->
### Erlang
```erlang
-module(example_app).
session_count() ->
% logic for calculating session count.
```
### Elixir
```elixir
defmodule ExampleApp do
def session_count do
# logic for calculating session count
end
end
```
<!-- tabs-close -->
To achieve that, we need a measurement dispatching the value we're interested in:
<!-- tabs-open -->
### Erlang
```erlang
-module(example_app_measurements).
dispatch_session_count() ->
telemetry:execute([example_app, session_count], example_app:session_count()).
```
### Elixir
```elixir
defmodule ExampleApp.Measurements do
def dispatch_session_count do
:telemetry.execute([:example_app, :session_count], ExampleApp.session_count())
end
end
```
<!-- tabs-close -->
and tell the Poller to invoke it periodically:
<!-- tabs-open -->
### Erlang
```erlang
telemetry_poller:start_link([{measurements, [{example_app_measurements, dispatch_session_count, []}]).
```
### Elixir
```elixir
:telemetry_poller.start_link(measurements: [{ExampleApp.Measurements, :dispatch_session_count, []}])
```
<!-- tabs-close -->
If you find that you need to somehow label the event values, e.g. differentiate between number of
sessions of regular and admin users, you could use event metadata:
<!-- tabs-open -->
### Erlang
```erlang
-module(example_app_measurements).
dispatch_session_count() ->
Regulars = example_app:regular_users_session_count(),
Admins = example_app:admin_users_session_count(),
telemetry:execute([example_app, session_count], #{count => Admins}, #{role => admin}),
telemetry:execute([example_app, session_count], #{count => Regulars}, #{role => regular}).
```
### Elixir
```elixir
defmodule ExampleApp.Measurements do
def dispatch_session_count do
regulars = ExampleApp.regular_users_session_count()
admins = ExampleApp.admin_users_session_count()
:telemetry.execute([:example_app, :session_count], %{count: admins}, %{role: :admin})
:telemetry.execute([:example_app, :session_count], %{count: regulars}, %{role: :regular})
end
end
```
<!-- tabs-close -->
> #### Note {: .info}
>
> The other solution would be to dispatch two different events by hooking up
> `example_app:regular_users_session_count/0` and `example_app:admin_users_session_count/0`
> functions directly. However, if you add more and more user roles to your app, you'll find
> yourself creating a new event for each one of them, which will force you to modify existing
event handlers. If you can break down event value by some feature, like user role in this
example, it's usually better to use event metadata than add new events.
This is a perfect use case for poller, because you don't need to write a dedicated process
which would call these functions periodically. Additionally, if you find that you need to collect
more statistics like this in the future, you can easily hook them up to the same poller process
and avoid creating lots of processes which would stay idle most of the time.
""").
-behaviour(gen_server).
%% API
-export([
child_spec/1,
list_measurements/1,
start_link/1
]).
-export([code_change/3, handle_call/3, handle_cast/2,
handle_info/2, init/1, terminate/2]).
-export_type([
option/0,
options/0,
measurement/0,
period/0]).
-include_lib("kernel/include/logger.hrl").
?DOC("""
The reference to a poller process.
""").
-type t() :: gen_server:server_ref().
?DOC("""
A list of options for the poller.
""").
-type options() :: [option()].
?DOC("""
An option for the poller.
""").
-type option() ::
{name, atom() | gen_server:server_name()}
| {period, period()}
| {init_delay, init_delay()}
| {measurements, [measurement()]}.
?DOC("""
A measurement for the poller.
""").
-type measurement() ::
memory
| total_run_queue_lengths
| system_counts
| persistent_term
| {process_info, [{name, atom()} | {event, [atom()]} | {keys, [atom()]}]}
| {module(), atom(), list()}.
?DOC("""
A period for the poller.
""").
-type period() :: pos_integer().
?DOC("""
An init delay for the poller.
""").
-type init_delay() :: non_neg_integer().
-type state() :: #{measurements => [measurement()], period => period()}.
?DOC("""
Starts a poller linked to the calling process.
Useful for starting Pollers as a part of a supervision tree.
## Options
The default options are:
* `{name, telemetry_poller}`
* `{period, timer:seconds(5)}`
* `{init_delay, 0}`
""").
-spec start_link(options()) -> gen_server:start_ret().
start_link(Opts) when is_list(Opts) ->
Args = parse_args(Opts),
case lists:keyfind(name, 1, Opts) of
{name, Name} when is_atom(Name) -> gen_server:start_link({local, Name}, ?MODULE, Args, []);
{name, Name} -> gen_server:start_link(Name, ?MODULE, Args, []);
false -> gen_server:start_link(?MODULE, Args, [])
end.
?DOC("""
Returns a list of measurements used by the poller.
""").
-spec list_measurements(t()) -> [measurement()].
list_measurements(Poller) ->
gen_server:call(Poller, get_measurements).
?DOC(false).
-spec init(map()) -> {ok, state()}.
init(Args) ->
schedule_measurement(maps:get(init_delay, Args)),
{ok, #{
measurements => maps:get(measurements, Args),
period => maps:get(period, Args)}}.
?DOC("""
Returns a child spec for the poller for running under a supervisor.
""").
-spec child_spec(options()) -> supervisor:child_spec().
child_spec(Opts) ->
Id =
case proplists:get_value(name, Opts) of
undefined -> ?MODULE;
Name when is_atom(Name) -> Name;
{global, Name} -> Name;
{via, _, Name} -> Name
end,
#{
id => Id,
start => {telemetry_poller, start_link, [Opts]}
}.
parse_args(Args) ->
Measurements = proplists:get_value(measurements, Args, []),
Period = proplists:get_value(period, Args, timer:seconds(5)),
InitDelay = proplists:get_value(init_delay, Args, 0),
#{
measurements => parse_measurements(Measurements),
period => validate_period(Period),
init_delay => validate_init_delay(InitDelay)
}.
-spec schedule_measurement(non_neg_integer()) -> ok.
schedule_measurement(CollectInMillis) ->
erlang:send_after(CollectInMillis, self(), collect), ok.
-spec validate_period(term()) -> period() | no_return().
validate_period(Period) when is_integer(Period), Period > 0 ->
Period;
validate_period(Term) ->
erlang:error({badarg, "Expected period to be a positive integer"}, [Term]).
-spec validate_init_delay(term()) -> init_delay() | no_return().
validate_init_delay(InitDelay) when is_integer(InitDelay), InitDelay >= 0 ->
InitDelay;
validate_init_delay(Term) ->
erlang:error({badarg, "Expected init_delay to be 0 or a positive integer"}, [Term]).
-spec parse_measurements([measurement()]) -> [{module(), atom(), list()}].
parse_measurements(Measurements) when is_list(Measurements) ->
lists:map(fun parse_measurement/1, Measurements);
parse_measurements(Term) ->
erlang:error({badarg, "Expected measurements to be a list"}, [Term]).
-spec parse_measurement(measurement()) -> {module(), atom(), list()}.
parse_measurement(memory) ->
{telemetry_poller_builtin, memory, []};
parse_measurement(total_run_queue_lengths) ->
{telemetry_poller_builtin, total_run_queue_lengths, []};
parse_measurement(system_counts) ->
{telemetry_poller_builtin, system_counts, []};
parse_measurement(persistent_term) ->
{telemetry_poller_builtin, persistent_term, []};
parse_measurement({process_info, List}) when is_list(List) ->
Name = case proplists:get_value(name, List) of
undefined -> erlang:error({badarg, "Expected `name' key to be given under process_info measurement"});
PropName when is_atom(PropName) -> PropName;
PropName -> erlang:error({badarg, "Expected `name' key to be an atom under process_info measurement"}, [PropName])
end,
Event = case proplists:get_value(event, List) of
undefined -> erlang:error({badarg, "Expected `event' key to be given under process_info measurement"});
PropEvent when is_list(PropEvent) -> PropEvent;
PropEvent -> erlang:error({badarg, "Expected `event' key to be a list of atoms under process_info measurement"}, [PropEvent])
end,
Keys = case proplists:get_value(keys, List) of
undefined -> erlang:error({badarg, "Expected `keys' key to be given under process_info measurement"});
PropKeys when is_list(PropKeys) -> PropKeys;
PropKeys -> erlang:error({badarg, "Expected `keys' key to be a list of atoms under process_info measurement"}, [PropKeys])
end,
{telemetry_poller_builtin, process_info, [Event, Name, Keys]};
parse_measurement({M, F, A}) when is_atom(M), is_atom(F), is_list(A) ->
{M, F, A};
parse_measurement(Term) ->
erlang:error({badarg, "Expected measurement to be memory, total_run_queue_lengths, {process_info, list()}, or a {module(), function(), list()} tuple"}, [Term]).
-spec make_measurements_and_filter_misbehaving([measurement()]) -> [measurement()].
make_measurements_and_filter_misbehaving(Measurements) ->
[Measurement || Measurement <- Measurements, make_measurement(Measurement) =/= error].
-spec make_measurement(measurement()) -> measurement() | no_return().
make_measurement(Measurement = {M, F, A}) ->
try erlang:apply(M, F, A) of
_ -> Measurement
catch
Class:Reason:Stacktrace ->
?LOG_ERROR("Error when calling MFA defined by measurement: ~p ~p ~p~n"
"Class=~p~nReason=~p~nStacktrace=~p~n",
[M, F, A, Class, Reason, Stacktrace]),
error
end.
?DOC(false).
handle_call(get_measurements, _From, State = #{measurements := Measurements}) ->
{reply, Measurements, State};
handle_call(_Request, _From, State) ->
{reply, ok, State}.
?DOC(false).
handle_cast(_Msg, State) -> {noreply, State}.
?DOC(false).
handle_info(collect, State) ->
GoodMeasurements = make_measurements_and_filter_misbehaving(maps:get(measurements, State)),
schedule_measurement(maps:get(period, State)),
{noreply, State#{measurements := GoodMeasurements}};
handle_info(_, State) ->
{noreply, State}.
?DOC(false).
terminate(_Reason, _State) -> ok.
?DOC(false).
code_change(_OldVsn, State, _Extra) -> {ok, State}.

View File

@@ -0,0 +1,29 @@
%% @private
-module(telemetry_poller_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
PollerChildSpec =
case application:get_env(telemetry_poller, default, []) of
false ->
[];
PollerOpts ->
Default = #{
name => telemetry_poller_default,
measurements => [
memory,
total_run_queue_lengths,
system_counts,
persistent_term
]
},
FinalOpts = maps:to_list(maps:merge(Default, maps:from_list(PollerOpts))),
[telemetry_poller:child_spec(FinalOpts)]
end,
telemetry_poller_sup:start_link(PollerChildSpec).
stop(_State) ->
ok.

View File

@@ -0,0 +1,64 @@
%% @private
-module(telemetry_poller_builtin).
-export([
memory/0,
total_run_queue_lengths/0,
system_counts/0,
persistent_term/0,
process_info/3
]).
-spec process_info([atom()], atom(), [atom()]) -> ok.
process_info(Event, Name, Measurements) ->
case erlang:whereis(Name) of
undefined -> ok;
Pid ->
case erlang:process_info(Pid, Measurements) of
undefined -> ok;
Info -> telemetry:execute(Event, maps:from_list(Info), #{name => Name})
end
end.
-spec memory() -> ok.
memory() ->
Measurements = erlang:memory(),
telemetry:execute([vm, memory], maps:from_list(Measurements), #{}).
-spec total_run_queue_lengths() -> ok.
total_run_queue_lengths() ->
Total = cpu_stats(total),
CPU = cpu_stats(cpu),
telemetry:execute([vm, total_run_queue_lengths], #{
total => Total,
cpu => CPU,
io => Total - CPU},
#{}).
-spec cpu_stats(total | cpu) -> non_neg_integer().
cpu_stats(total) ->
erlang:statistics(total_run_queue_lengths_all);
cpu_stats(cpu) ->
erlang:statistics(total_run_queue_lengths).
-spec system_counts() -> ok.
system_counts() ->
ProcessCount = erlang:system_info(process_count),
AtomCount = erlang:system_info(atom_count),
PortCount = erlang:system_info(port_count),
ProcessLimit = erlang:system_info(process_limit),
AtomLimit = erlang:system_info(atom_limit),
PortLimit = erlang:system_info(port_limit),
telemetry:execute([vm, system_counts], #{
process_count => ProcessCount,
atom_count => AtomCount,
port_count => PortCount,
process_limit => ProcessLimit,
atom_limit => AtomLimit,
port_limit => PortLimit
}).
-spec persistent_term() -> ok.
persistent_term() ->
Info = persistent_term:info(),
telemetry:execute([vm, persistent_term], Info, #{}).

View File

@@ -0,0 +1,18 @@
%% @private
-module(telemetry_poller_sup).
-behaviour(supervisor).
-export([start_link/1]).
-export([init/1]).
-define(SERVER, ?MODULE).
start_link(PollerChildSpec) ->
supervisor:start_link({local, ?SERVER}, ?MODULE, PollerChildSpec).
init(PollerChildSpec) ->
SupFlags = #{strategy => one_for_one,
intensity => 1,
period => 5},
{ok, {SupFlags, PollerChildSpec}}.