├── .codecov.yml ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── NOTICE ├── README.md ├── examples ├── TelemetryPollerVM.ex └── telemetry_poller_vm.erl ├── rebar.config ├── rebar.lock ├── src ├── telemetry_poller.app.src ├── telemetry_poller.erl ├── telemetry_poller_app.erl ├── telemetry_poller_builtin.erl └── telemetry_poller_sup.erl └── test ├── support ├── test_handler.erl └── test_measure.erl └── telemetry_poller_SUITE.erl /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | behaviour: new 3 | require_changes: yes 4 | 5 | ignore: 6 | - "test/support/.*" -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'main' 7 | push: 8 | branches: 9 | - 'main' 10 | 11 | jobs: 12 | build: 13 | name: Test on OTP ${{ matrix.otp_version }} and ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | otp_version: [24, 25, 26, 27] 18 | os: [ubuntu-latest] 19 | env: 20 | OTP_VERSION: ${{ matrix.otp_version }} 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: erlef/setup-beam@v1 24 | with: 25 | otp-version: ${{ matrix.otp_version }} 26 | rebar3-version: 3.22.0 27 | - uses: actions/cache@v4 28 | name: Cache 29 | with: 30 | path: | 31 | _build 32 | key: ${{ runner.os }}-build-${{ matrix.otp_version }}-${{ hashFiles(format('rebar.lock')) }}-1 33 | restore-keys: | 34 | ${{ runner.os }}-build-${{ matrix.otp_version }}-1- 35 | - name: Report 36 | run: rebar3 report "compile" 37 | - name: Compile 38 | run: rebar3 compile 39 | - name: Common Test tests 40 | run: rebar3 ct --cover 41 | - name: XRef 42 | run: rebar3 xref 43 | - name: Dialyzer 44 | run: rebar3 dialyzer 45 | if: ${{ matrix.otp_version == 27 }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.rebar3 2 | /_build 3 | /deps 4 | /log 5 | /doc 6 | erl_crash.dump 7 | rebar3.crashdump 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.2.0](https://github.com/beam-telemetry/telemetry_poller/tree/v1.2.0) 9 | 10 | ### Added 11 | 12 | - Support `persistent_term` measurements. 13 | - Require Erlang/OTP 24+. 14 | 15 | ## [1.1.0](https://github.com/beam-telemetry/telemetry_poller/tree/v1.1.0) 16 | 17 | ### Added 18 | 19 | - Add the `init_delay` configuration option. (#70) 20 | 21 | ## [1.0.0](https://github.com/beam-telemetry/telemetry_poller/tree/v1.0.0) 22 | 23 | This release marks stability of the API. The library now requires Telemetry ~> 1.0. 24 | 25 | ### Fixed 26 | 27 | - Fix disabling the default poller via application environment. 28 | 29 | ### Changed 30 | 31 | - Drop support for OTP 20. 32 | 33 | ## [0.5.1](https://github.com/beam-telemetry/telemetry_poller/tree/v0.5.1) 34 | 35 | ### Added 36 | 37 | - Support `{:global, atom()}` and `{:via, module(), term()}` names for the poller process. 38 | 39 | ## [0.5.0](https://github.com/beam-telemetry/telemetry_poller/tree/v0.5.0) 40 | 41 | ### Added 42 | 43 | - `system_counts` measurement 44 | 45 | ## [0.4.1](https://github.com/beam-telemetry/telemetry_poller/tree/v0.4.1) 46 | 47 | ### Changed 48 | 49 | - Improve docs 50 | - No longer add a default name to telemetry processes 51 | 52 | ## [0.4.0](https://github.com/beam-telemetry/telemetry_poller/tree/v0.4.0) 53 | 54 | Telemetry Poller has been rewritten in Erlang so it can be used by the overall Erlang community. 55 | Therefore, the `Telemetry.Poller` module must now be accessed as `telemetry_poller`. 56 | A new `process_info` measurement has also been added and the `vm_measurements` and `measurements` 57 | keys have been merged into a single `measurements` key for simplicity. 58 | 59 | ### Added 60 | 61 | - `:process_info` measurement 62 | 63 | ### Changed 64 | 65 | - `vm_measurements` and `measurements` have been merged into `measurements` 66 | - `Telemetry.Poller` has been rewritten to `telemetry_poller` 67 | 68 | ## [0.3.0](https://github.com/beam-telemetry/telemetry_poller/tree/v0.3.0) 69 | 70 | This release marks the upgrade to Telemetry 0.4.0. This means that Poller measurements can emit a map 71 | of values now instead of a single one, making it less "noisy" when it comes to number of emitted events. 72 | 73 | All specific memory measurements have been replaced with a single `:memory` measurement sending all 74 | the values that were emitted by old measurements at once. 75 | 76 | `:run_queue_lengths` VM measurement has been removed for now, as we believe that as detailed data 77 | as it provided is not necessary to effectively debug the system. `:total_run_queue_lengths` VM 78 | measurement has been changed so that it reports a `:total` length of run queues, length of `:cpu` 79 | run queues (including dirty CPU run queue), and length of (dirty) `:io` run queue. 80 | 81 | ### Added 82 | 83 | - `:memory` VM measurement reporting all the data returned by `:erlang.memory/0` call. 84 | 85 | ### Changed 86 | 87 | - `:total_run_queue_lengths` VM measurement is reporting a `:total`, `:cpu` and `:io` run queue lengths 88 | now. See documentation for more details. 89 | 90 | ### Removed 91 | 92 | - `:total_memory`, `:atom_memory`, `:atom_used_memory`, `:processes_memory`, `:processes_used_memory`, 93 | `:binary_memory`, `:ets_memory`, `:code_memory` and `:system_memory` VM measurements have been removed. 94 | Please use the `:memory` measurement now instead. 95 | 96 | ## [0.2.0](https://github.com/beam-telemetry/telemetry_poller/tree/v0.2.0) 97 | 98 | ### Added 99 | 100 | - Added `:total_run_queue_lengths` and `:run_queue_lengths` memory measurements; 101 | 102 | ### Changed 103 | 104 | - `:total_run_queue_lengths` is now included in the set of default VM measurements; 105 | - A default Poller process, with a default set of VM measurements, is started when `:telemetry_poller` 106 | application starts (configurable via `:telemetry.poller, :default` application environment). 107 | - VM measurements are now provided to Poller's `:vm_measurements` option. Passing atom `:default` 108 | to this option makes the Poller use a default set of VM measurements; 109 | - Telemetry has been upgraded to version 0.3, meaning that VM measurements now use this version to 110 | emit the events. 111 | 112 | ### Removed 113 | 114 | - `Telemetry.Poller.vm_measurements/0` function has been removed in favor of `:vm_measurements` 115 | option. 116 | 117 | ### Fixed 118 | 119 | - Fixed the type definition of `Telemetry.Poller.measurement/0` type - no Dialyzer warnings are 120 | emitted when running it on the project. 121 | 122 | ## [0.1.0](https://github.com/beam-telemetry/telemetry_poller/tree/v0.1.0) 123 | 124 | ### Added 125 | 126 | - The Poller process periodically invoking registered functions which emit Telemetry events. 127 | It supports VM memory measurements and custom measurements provided as MFAs. 128 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Chris McCord and Erlang Solutions 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # telemetry_poller 2 | 3 | [![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) 4 | [![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) 5 | 6 | Allows to periodically collect measurements and dispatch them as Telemetry events. 7 | 8 | `telemetry_poller` by default runs a poller to perform VM measurements: 9 | 10 | * `[vm, memory]` - contains the total memory, process memory, and all other keys in `erlang:memory/0` 11 | * `[vm, total_run_queue_lengths]` - returns the run queue lengths for CPU and IO schedulers. It contains the `total`, `cpu` and `io` measurements 12 | * `[vm, system_counts]` - returns the current process, atom and port count as per `erlang:system_info/1` 13 | * `[vm, persistent_term]` - number of terms and memory byte size for `persistent_term` 14 | 15 | You can directly consume those events after adding `telemetry_poller` as a dependency. 16 | 17 | Poller also provides a convenient API for running custom pollers. 18 | 19 | ## Defining custom measurements 20 | 21 | Poller also includes conveniences for performing process-based measurements as well as custom ones. 22 | 23 | ### Erlang 24 | 25 | 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: 26 | 27 | ```erlang 28 | telemetry_poller:start_link( 29 | [{measurements, [ 30 | {process_info, [{name, my_app_worker}, {event, [my_app, worker]}, {keys, [memory, message_queue_len]}]}, 31 | {example_app_measurements, dispatch_session_count, []} 32 | ]}, 33 | {period, timer:seconds(10)}, % configure sampling period - default is timer:seconds(5) 34 | {init_delay, timer:seconds(600)}, % configure sampling initial delay - default is 0 35 | {name, my_app_poller} 36 | ]). 37 | ``` 38 | 39 | Now define the custom measurement and you are good to go: 40 | 41 | ```erlang 42 | -module(example_app_measurements). 43 | 44 | dispatch_session_count() -> 45 | % emit a telemetry event when called 46 | telemetry:execute([example_app, session_count], #{count => example_app:session_count()}, #{}). 47 | ``` 48 | 49 | ### Elixir 50 | 51 | You typically start the poller as a child in your supervision tree: 52 | 53 | ```elixir 54 | children = [ 55 | {:telemetry_poller, 56 | # include custom measurement as an MFA tuple 57 | measurements: [ 58 | {:process_info, name: :my_app_worker, event: [:my_app, :worker], keys: [:memory, :message_queue_len]}, 59 | {ExampleApp.Measurements, :dispatch_session_count, []}, 60 | ], 61 | period: :timer.seconds(10), # configure sampling period - default is :timer.seconds(5) 62 | init_delay: :timer.seconds(600), # configure sampling initial delay - default is 0 63 | name: :my_app_poller} 64 | ] 65 | 66 | Supervisor.start_link(children, strategy: :one_for_one) 67 | ``` 68 | 69 | 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: 70 | 71 | ```elixir 72 | defmodule ExampleApp.Measurements do 73 | def dispatch_session_count() do 74 | # emit a telemetry event when called 75 | :telemetry.execute([:example_app, :session_count], %{count: ExampleApp.session_count()}, %{}) 76 | end 77 | end 78 | ``` 79 | 80 | ## Documentation 81 | 82 | See [documentation](https://hexdocs.pm/telemetry_poller/) for more concrete examples and usage 83 | instructions. 84 | 85 | ## VM metrics example 86 | 87 | ### Erlang 88 | 89 | Find, in `examples/telemetry_poller_vm.erl`, an example on how to retrieve to VM measurements, 90 | mentioned above. 91 | 92 | To see it in action, fire up `rebar3 shell`, then 93 | 94 | ```erlang 95 | {ok, telemetry_poller_vm} = c("examples/telemetry_poller_vm"). 96 | ok = file:delete("telemetry_poller_vm.beam"). % Deletes generated BEAM 97 | ok = telemetry_poller_vm:attach(). 98 | ``` 99 | 100 | ### Elixir 101 | 102 | Find, in `examples/TelemetryPollerVM.ex`, an example on how to retrieve to VM measurements, 103 | mentioned above. 104 | 105 | To see it in action, first compile the Erlang sources with `rebar3 compile`. 106 | 107 | Then fire up `iex -pa "_build/default/lib/*/ebin"`, then 108 | 109 | ```elixir 110 | {:ok, _} = Application.ensure_all_started(:telemetry_poller) 111 | 112 | [TelemetryPollerVM] = c("examples/TelemetryPollerVM.ex") 113 | :ok = TelemetryPollerVM.attach() 114 | ``` 115 | 116 | ## Copyright and License 117 | 118 | Copyright (c) 2025 Erlang Ecosystem Foundation. 119 | 120 | telemetry_poller source code is released under Apache License, Version 2.0. 121 | 122 | See [LICENSE](LICENSE) and [NOTICE](NOTICE) files for more information. 123 | -------------------------------------------------------------------------------- /examples/TelemetryPollerVM.ex: -------------------------------------------------------------------------------- 1 | defmodule TelemetryPollerVM do 2 | def attach do 3 | Enum.each( 4 | [ 5 | [:vm, :memory], 6 | [:vm, :total_run_queue_lengths], 7 | [:vm, :system_counts] 8 | ], 9 | fn [:vm, namespace] = event_name -> 10 | handler_id = "vm.#{Atom.to_string(namespace)}" 11 | 12 | :telemetry.attach(handler_id, event_name, &TelemetryPollerVM.handle/4, _config = []) 13 | end 14 | ) 15 | end 16 | 17 | def handle( 18 | [:vm, :memory], 19 | event_measurements, 20 | _event_metadata, 21 | _handler_config 22 | ) do 23 | # Do something with the measurements 24 | IO.puts( 25 | "memory\n" <> 26 | "------\n" <> 27 | " atom: #{event_measurements.atom}\n" <> 28 | " atom_used: #{event_measurements.atom_used}\n" <> 29 | " binary: #{event_measurements.binary}\n" <> 30 | " code: #{event_measurements.code}\n" <> 31 | " ets: #{event_measurements.ets}\n" <> 32 | " processes: #{event_measurements.processes}\n" <> 33 | " processes_used: #{event_measurements.processes_used}\n" <> 34 | " system: #{event_measurements.system}\n" <> 35 | " total: #{event_measurements.total}\n" 36 | ) 37 | end 38 | 39 | def handle( 40 | [:vm, :total_run_queue_lengths], 41 | event_measurements, 42 | _event_metadata, 43 | _handler_config 44 | ) do 45 | # Do something with the measurements 46 | IO.puts( 47 | "total_run_queue_lengths\n" <> 48 | "-----------------------\n" <> 49 | " cpu: #{event_measurements.cpu}\n" <> 50 | " io: #{event_measurements.io}\n" <> 51 | " total: #{event_measurements.total}\n" 52 | ) 53 | end 54 | 55 | def handle( 56 | [:vm, :system_counts], 57 | event_measurements, 58 | _event_metadata, 59 | _handler_config 60 | ) do 61 | # Do something with the measurements 62 | IO.puts( 63 | "system_counts\n" <> 64 | "-------------\n" <> 65 | " atom_count: #{event_measurements.atom_count}\n" <> 66 | " port_count: #{event_measurements.port_count}\n" <> 67 | " process_count: #{event_measurements.process_count}\n" 68 | ) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /examples/telemetry_poller_vm.erl: -------------------------------------------------------------------------------- 1 | -module(telemetry_poller_vm). 2 | 3 | -export([attach/0]). 4 | -export([handle/4]). 5 | 6 | attach() -> 7 | lists:foreach( 8 | fun([vm, Namespace] = EventName) -> 9 | NamespaceBin = atom_to_binary(Namespace), 10 | HandlerId = <<"vm.", NamespaceBin/binary>>, 11 | telemetry:attach(HandlerId, EventName, fun telemetry_poller_vm:handle/4, _Config = []) 12 | end, 13 | [ 14 | [vm, memory], 15 | [vm, total_run_queue_lengths], 16 | [vm, system_counts] 17 | ] 18 | ). 19 | 20 | handle([vm, memory], EventMeasurements, _EventMetadata, _HandlerConfig) -> 21 | #{ 22 | atom := Atom, 23 | atom_used := AtomUser, 24 | binary := Binary, 25 | code := Code, 26 | ets := ETS, 27 | processes := Processes, 28 | processes_used := ProcessesUsed, 29 | system := System, 30 | total := Total 31 | } = EventMeasurements, 32 | % Do something with the measurements 33 | io:format( 34 | "memory~n" 35 | "------~n" 36 | " atom: ~p~n" 37 | " atom_used: ~p~n" 38 | " binary: ~p~n" 39 | " code: ~p~n" 40 | " ets: ~p~n" 41 | " processes: ~p~n" 42 | " processes_used: ~p~n" 43 | " system: ~p~n" 44 | " total: ~p~n~n", 45 | [ 46 | Atom, 47 | AtomUser, 48 | Binary, 49 | Code, 50 | ETS, 51 | Processes, 52 | ProcessesUsed, 53 | System, 54 | Total 55 | ] 56 | ); 57 | handle([vm, total_run_queue_lengths], EventMeasurements, _EventMetadata, _HandlerConfig) -> 58 | #{ 59 | cpu := CPU, 60 | io := IO, 61 | total := Total 62 | } = EventMeasurements, 63 | % Do something with the measurements 64 | io:format( 65 | "total_run_queue_lengths~n" 66 | "-----------------------~n" 67 | " cpu: ~p~n" 68 | " io: ~p~n" 69 | " total: ~p~n~n", 70 | [ 71 | CPU, 72 | IO, 73 | Total 74 | ] 75 | ); 76 | handle([vm, system_counts], EventMeasurements, _EventMetadata, _HandlerConfig) -> 77 | #{ 78 | atom_count := AtomCount, 79 | port_count := PortCount, 80 | process_count := ProcessCount 81 | } = EventMeasurements, 82 | % Do something with the measurements 83 | io:format( 84 | "system_counts~n" 85 | "-------------~n" 86 | " atom_count: ~p~n" 87 | " port_count: ~p~n" 88 | " process_count: ~p~n~n", 89 | [ 90 | AtomCount, 91 | PortCount, 92 | ProcessCount 93 | ] 94 | ). 95 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {minimum_otp_vsn, "24.0"}. 2 | 3 | {erl_opts, [debug_info]}. 4 | 5 | {deps, [ 6 | {telemetry, "~> 1.0"} 7 | ]}. 8 | 9 | {profiles, [ 10 | {test, [ 11 | {erl_opts, [nowarn_export_all]}, 12 | %% create junit xml for circleci 13 | {ct_opts, [{ct_hooks, [cth_surefire]}]}, 14 | {src_dirs, ["src", "test/support"]} 15 | ]} 16 | ]}. 17 | 18 | {shell, [{apps, [telemetry_poller]}]}. 19 | 20 | {project_plugins, [rebar3_ex_doc]}. 21 | {ex_doc, [ 22 | {main, "README"}, 23 | {extras, [<<"README.md">>, <<"CHANGELOG.md">>, <<"LICENSE">>, <<"NOTICE">>]}, 24 | {source_url, <<"https://github.com/beam-telemetry/telemetry_poller">>}, 25 | {source_ref, <<"v1.2.0">>} 26 | ]}. 27 | {hex, [ 28 | {doc, #{provider => ex_doc}} 29 | ]}. 30 | 31 | %% take out warnings for unused exported functions 32 | {xref_checks,[undefined_function_calls, undefined_functions, locals_not_used, 33 | deprecated_function_calls, deprecated_functions]}. 34 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.0.0">>},0}]}. 3 | [ 4 | {pkg_hash,[ 5 | {<<"telemetry">>, <<"0F453A102CDF13D506B7C0AB158324C337C41F1CC7548F0BC0E130BBF0AE9452">>}]}, 6 | {pkg_hash_ext,[ 7 | {<<"telemetry">>, <<"73BC09FA59B4A0284EFB4624335583C528E07EC9AE76ACA96EA0673850AEC57A">>}]} 8 | ]. 9 | -------------------------------------------------------------------------------- /src/telemetry_poller.app.src: -------------------------------------------------------------------------------- 1 | {application, telemetry_poller, 2 | [{description, "Periodically collect measurements and dispatch them as Telemetry events."}, 3 | {vsn, "git"}, 4 | {registered, []}, 5 | {mod, {telemetry_poller_app, []}}, 6 | {applications, 7 | [kernel, 8 | stdlib, 9 | telemetry 10 | ]}, 11 | {env,[]}, 12 | {modules, []}, 13 | {licenses, ["Apache-2.0"]}, 14 | {doc, "doc"}, 15 | {links, [{"GitHub", "https://github.com/beam-telemetry/telemetry_poller"}]} 16 | ]}. 17 | -------------------------------------------------------------------------------- /src/telemetry_poller.erl: -------------------------------------------------------------------------------- 1 | -if(?OTP_RELEASE >= 27). 2 | -define(MODULEDOC(Str), -moduledoc(Str)). 3 | -define(DOC(Str), -doc(Str)). 4 | -else. 5 | -define(MODULEDOC(Str), -compile([])). 6 | -define(DOC(Str), -compile([])). 7 | -endif. 8 | 9 | -module(telemetry_poller). 10 | 11 | ?MODULEDOC(""" 12 | A time-based poller to periodically dispatch Telemetry events. 13 | 14 | A poller is a process start in your supervision tree with a list 15 | of measurements to perform periodically. On start it expects the 16 | period in milliseconds and a list of measurements to perform. Initial delay 17 | is an optional parameter that sets time delay in milliseconds before starting 18 | measurements: 19 | 20 | 21 | 22 | ### Erlang 23 | 24 | ``` 25 | telemetry_poller:start_link([ 26 | {measurements, Measurements}, 27 | {period, Period}, 28 | {init_delay, InitDelay} 29 | ]) 30 | ``` 31 | 32 | ### Elixir 33 | 34 | ``` 35 | :telemetry_poller.start_link( 36 | measurements: measurements, 37 | period: period, 38 | init_delay: init_delay 39 | ) 40 | ``` 41 | 42 | 43 | 44 | ## Measurements 45 | 46 | The following measurements are supported: 47 | 48 | * `memory` (default) 49 | * `total_run_queue_lengths` (default) 50 | * `system_counts` (default) 51 | * `persistent_term` (default) 52 | * `{process_info, Proplist}` 53 | * `{Module, Function, Args}` 54 | 55 | We will discuss each measurement in detail. Also note that the 56 | `telemetry_poller` application ships with a built-in poller that 57 | measures `memory`, `total_run_queue_lengths`, `system_counts`, and `persistent_term`. This takes 58 | the VM measurement out of the way so your application can focus 59 | on what is specific to its behaviour. 60 | 61 | ### Memory 62 | 63 | An event emitted as `[vm, memory]`. The measurement includes all 64 | the key-value pairs returned by the `erlang:memory/0` function, 65 | e.g. `total` for total memory, `processes_used` for memory used by 66 | all processes, and so on. 67 | 68 | ### Total run queue lengths 69 | 70 | On startup, the Erlang VM starts many schedulers to do both IO and 71 | CPU work. If a process needs to do some work or wait on IO, it is 72 | allocated to the appropriate scheduler. The measurement includes the 73 | following keys: 74 | 75 | * `total` - all schedulers (CPU + IO) 76 | * `cpu` - CPU schedulers 77 | * `io` - IO schedulers 78 | 79 | ### System counts 80 | 81 | The measurement includes: 82 | 83 | * `process_count` - the number of processes currently existing at the local node 84 | * `atom_count` - the number of atoms currently existing at the local node 85 | * `port_count` - the number of ports currently existing at the local node 86 | 87 | ### Persistent term (since 1.2.0) 88 | 89 | An event emitted as `[vm, persistent_term]`. The measurement includes information 90 | about persistent terms in the system, as returned by `persistent_term:info/0`: 91 | 92 | * `count` - The number of persistent terms 93 | * `memory` - The total amount of memory (measured in bytes) used by all persistent terms 94 | 95 | ### Process info 96 | 97 | A measurement with information about a given process. It must be specified 98 | alongside a proplist with the process name, the event name, and a list of 99 | keys to be included: 100 | 101 | 102 | 103 | ### Erlang 104 | 105 | ```erlang 106 | {process_info, [ 107 | {name, my_app_worker}, 108 | {event, [my_app, worker]}, 109 | {keys, [message_queue_len, memory]} 110 | ]} 111 | ``` 112 | 113 | ### Elixir 114 | 115 | ```elixir 116 | {:process_info, [ 117 | name: my_app_worker, 118 | event: [my_app, worker], 119 | keys: [message_queue_len, memory] 120 | ]} 121 | ``` 122 | 123 | 124 | 125 | ### Custom measurements 126 | 127 | Telemetry poller also allows you to perform custom measurements by passing 128 | a module-function-args tuple: 129 | 130 | 131 | 132 | ### Erlang 133 | 134 | ```erlang 135 | {my_app_example, measure, []} 136 | ``` 137 | 138 | ### Elixir 139 | 140 | ```elixir 141 | {MyApp.Example, :measure, []} 142 | ``` 143 | 144 | 145 | 146 | The given function will be invoked periodically and they must explicitly invoke the 147 | `telemetry:execute/3` function. If the invocation of the MFA fails, 148 | the measurement is removed from the Poller. 149 | 150 | For all options, see `start_link/1`. The options listed there can be given 151 | to the default poller as well as to custom pollers. 152 | 153 | ### Default poller 154 | 155 | A default poller is started with `telemetry_poller` responsible for emitting 156 | measurements for `memory` and `total_run_queue_lengths`. You can customize 157 | the behaviour of the default poller by setting the `default` key under the 158 | `telemetry_poller` application environment. Setting it to `false` disables 159 | the poller. 160 | 161 | ## Examples 162 | 163 | ### Example 1: tracking number of active sessions in web application 164 | 165 | Let's imagine that you have a web application and you would like to periodically 166 | measure number of active user sessions. 167 | 168 | 169 | 170 | ### Erlang 171 | 172 | ```erlang 173 | -module(example_app). 174 | 175 | session_count() -> 176 | % logic for calculating session count. 177 | ``` 178 | 179 | ### Elixir 180 | 181 | ```elixir 182 | defmodule ExampleApp do 183 | def session_count do 184 | # logic for calculating session count 185 | end 186 | end 187 | ``` 188 | 189 | 190 | 191 | To achieve that, we need a measurement dispatching the value we're interested in: 192 | 193 | 194 | 195 | ### Erlang 196 | 197 | ```erlang 198 | -module(example_app_measurements). 199 | 200 | dispatch_session_count() -> 201 | telemetry:execute([example_app, session_count], example_app:session_count()). 202 | ``` 203 | 204 | ### Elixir 205 | 206 | ```elixir 207 | defmodule ExampleApp.Measurements do 208 | def dispatch_session_count do 209 | :telemetry.execute([:example_app, :session_count], ExampleApp.session_count()) 210 | end 211 | end 212 | ``` 213 | 214 | 215 | 216 | and tell the Poller to invoke it periodically: 217 | 218 | 219 | 220 | ### Erlang 221 | 222 | ```erlang 223 | telemetry_poller:start_link([{measurements, [{example_app_measurements, dispatch_session_count, []}]). 224 | ``` 225 | 226 | ### Elixir 227 | 228 | ```elixir 229 | :telemetry_poller.start_link(measurements: [{ExampleApp.Measurements, :dispatch_session_count, []}]) 230 | ``` 231 | 232 | 233 | 234 | If you find that you need to somehow label the event values, e.g. differentiate between number of 235 | sessions of regular and admin users, you could use event metadata: 236 | 237 | 238 | 239 | ### Erlang 240 | 241 | ```erlang 242 | -module(example_app_measurements). 243 | 244 | dispatch_session_count() -> 245 | Regulars = example_app:regular_users_session_count(), 246 | Admins = example_app:admin_users_session_count(), 247 | telemetry:execute([example_app, session_count], #{count => Admins}, #{role => admin}), 248 | telemetry:execute([example_app, session_count], #{count => Regulars}, #{role => regular}). 249 | ``` 250 | 251 | ### Elixir 252 | 253 | ```elixir 254 | defmodule ExampleApp.Measurements do 255 | def dispatch_session_count do 256 | regulars = ExampleApp.regular_users_session_count() 257 | admins = ExampleApp.admin_users_session_count() 258 | :telemetry.execute([:example_app, :session_count], %{count: admins}, %{role: :admin}) 259 | :telemetry.execute([:example_app, :session_count], %{count: regulars}, %{role: :regular}) 260 | end 261 | end 262 | ``` 263 | 264 | 265 | 266 | > #### Note {: .info} 267 | > 268 | > The other solution would be to dispatch two different events by hooking up 269 | > `example_app:regular_users_session_count/0` and `example_app:admin_users_session_count/0` 270 | > functions directly. However, if you add more and more user roles to your app, you'll find 271 | > yourself creating a new event for each one of them, which will force you to modify existing 272 | event handlers. If you can break down event value by some feature, like user role in this 273 | example, it's usually better to use event metadata than add new events. 274 | 275 | This is a perfect use case for poller, because you don't need to write a dedicated process 276 | which would call these functions periodically. Additionally, if you find that you need to collect 277 | more statistics like this in the future, you can easily hook them up to the same poller process 278 | and avoid creating lots of processes which would stay idle most of the time. 279 | """). 280 | 281 | -behaviour(gen_server). 282 | 283 | %% API 284 | -export([ 285 | child_spec/1, 286 | list_measurements/1, 287 | start_link/1 288 | ]). 289 | 290 | -export([code_change/3, handle_call/3, handle_cast/2, 291 | handle_info/2, init/1, terminate/2]). 292 | 293 | -export_type([ 294 | option/0, 295 | options/0, 296 | measurement/0, 297 | period/0]). 298 | 299 | -include_lib("kernel/include/logger.hrl"). 300 | 301 | ?DOC(""" 302 | The reference to a poller process. 303 | """). 304 | -type t() :: gen_server:server_ref(). 305 | 306 | ?DOC(""" 307 | A list of options for the poller. 308 | """). 309 | -type options() :: [option()]. 310 | 311 | ?DOC(""" 312 | An option for the poller. 313 | """). 314 | -type option() :: 315 | {name, atom() | gen_server:server_name()} 316 | | {period, period()} 317 | | {init_delay, init_delay()} 318 | | {measurements, [measurement()]}. 319 | 320 | ?DOC(""" 321 | A measurement for the poller. 322 | """). 323 | -type measurement() :: 324 | memory 325 | | total_run_queue_lengths 326 | | system_counts 327 | | persistent_term 328 | | {process_info, [{name, atom()} | {event, [atom()]} | {keys, [atom()]}]} 329 | | {module(), atom(), list()}. 330 | 331 | ?DOC(""" 332 | A period for the poller. 333 | """). 334 | -type period() :: pos_integer(). 335 | 336 | ?DOC(""" 337 | An init delay for the poller. 338 | """). 339 | -type init_delay() :: non_neg_integer(). 340 | 341 | -type state() :: #{measurements => [measurement()], period => period()}. 342 | 343 | ?DOC(""" 344 | Starts a poller linked to the calling process. 345 | 346 | Useful for starting Pollers as a part of a supervision tree. 347 | 348 | ## Options 349 | 350 | The default options are: 351 | 352 | * `{name, telemetry_poller}` 353 | * `{period, timer:seconds(5)}` 354 | * `{init_delay, 0}` 355 | 356 | """). 357 | -spec start_link(options()) -> gen_server:start_ret(). 358 | start_link(Opts) when is_list(Opts) -> 359 | Args = parse_args(Opts), 360 | 361 | case lists:keyfind(name, 1, Opts) of 362 | {name, Name} when is_atom(Name) -> gen_server:start_link({local, Name}, ?MODULE, Args, []); 363 | {name, Name} -> gen_server:start_link(Name, ?MODULE, Args, []); 364 | false -> gen_server:start_link(?MODULE, Args, []) 365 | end. 366 | 367 | ?DOC(""" 368 | Returns a list of measurements used by the poller. 369 | """). 370 | -spec list_measurements(t()) -> [measurement()]. 371 | list_measurements(Poller) -> 372 | gen_server:call(Poller, get_measurements). 373 | 374 | ?DOC(false). 375 | -spec init(map()) -> {ok, state()}. 376 | init(Args) -> 377 | schedule_measurement(maps:get(init_delay, Args)), 378 | {ok, #{ 379 | measurements => maps:get(measurements, Args), 380 | period => maps:get(period, Args)}}. 381 | ?DOC(""" 382 | Returns a child spec for the poller for running under a supervisor. 383 | """). 384 | -spec child_spec(options()) -> supervisor:child_spec(). 385 | child_spec(Opts) -> 386 | Id = 387 | case proplists:get_value(name, Opts) of 388 | undefined -> ?MODULE; 389 | Name when is_atom(Name) -> Name; 390 | {global, Name} -> Name; 391 | {via, _, Name} -> Name 392 | end, 393 | 394 | #{ 395 | id => Id, 396 | start => {telemetry_poller, start_link, [Opts]} 397 | }. 398 | 399 | parse_args(Args) -> 400 | Measurements = proplists:get_value(measurements, Args, []), 401 | Period = proplists:get_value(period, Args, timer:seconds(5)), 402 | InitDelay = proplists:get_value(init_delay, Args, 0), 403 | #{ 404 | measurements => parse_measurements(Measurements), 405 | period => validate_period(Period), 406 | init_delay => validate_init_delay(InitDelay) 407 | }. 408 | 409 | -spec schedule_measurement(non_neg_integer()) -> ok. 410 | schedule_measurement(CollectInMillis) -> 411 | erlang:send_after(CollectInMillis, self(), collect), ok. 412 | 413 | -spec validate_period(term()) -> period() | no_return(). 414 | validate_period(Period) when is_integer(Period), Period > 0 -> 415 | Period; 416 | validate_period(Term) -> 417 | erlang:error({badarg, "Expected period to be a positive integer"}, [Term]). 418 | 419 | -spec validate_init_delay(term()) -> init_delay() | no_return(). 420 | validate_init_delay(InitDelay) when is_integer(InitDelay), InitDelay >= 0 -> 421 | InitDelay; 422 | validate_init_delay(Term) -> 423 | erlang:error({badarg, "Expected init_delay to be 0 or a positive integer"}, [Term]). 424 | 425 | -spec parse_measurements([measurement()]) -> [{module(), atom(), list()}]. 426 | parse_measurements(Measurements) when is_list(Measurements) -> 427 | lists:map(fun parse_measurement/1, Measurements); 428 | parse_measurements(Term) -> 429 | erlang:error({badarg, "Expected measurements to be a list"}, [Term]). 430 | 431 | -spec parse_measurement(measurement()) -> {module(), atom(), list()}. 432 | parse_measurement(memory) -> 433 | {telemetry_poller_builtin, memory, []}; 434 | parse_measurement(total_run_queue_lengths) -> 435 | {telemetry_poller_builtin, total_run_queue_lengths, []}; 436 | parse_measurement(system_counts) -> 437 | {telemetry_poller_builtin, system_counts, []}; 438 | parse_measurement(persistent_term) -> 439 | {telemetry_poller_builtin, persistent_term, []}; 440 | parse_measurement({process_info, List}) when is_list(List) -> 441 | Name = case proplists:get_value(name, List) of 442 | undefined -> erlang:error({badarg, "Expected `name' key to be given under process_info measurement"}); 443 | PropName when is_atom(PropName) -> PropName; 444 | PropName -> erlang:error({badarg, "Expected `name' key to be an atom under process_info measurement"}, [PropName]) 445 | end, 446 | 447 | Event = case proplists:get_value(event, List) of 448 | undefined -> erlang:error({badarg, "Expected `event' key to be given under process_info measurement"}); 449 | PropEvent when is_list(PropEvent) -> PropEvent; 450 | PropEvent -> erlang:error({badarg, "Expected `event' key to be a list of atoms under process_info measurement"}, [PropEvent]) 451 | end, 452 | 453 | Keys = case proplists:get_value(keys, List) of 454 | undefined -> erlang:error({badarg, "Expected `keys' key to be given under process_info measurement"}); 455 | PropKeys when is_list(PropKeys) -> PropKeys; 456 | PropKeys -> erlang:error({badarg, "Expected `keys' key to be a list of atoms under process_info measurement"}, [PropKeys]) 457 | end, 458 | 459 | {telemetry_poller_builtin, process_info, [Event, Name, Keys]}; 460 | parse_measurement({M, F, A}) when is_atom(M), is_atom(F), is_list(A) -> 461 | {M, F, A}; 462 | parse_measurement(Term) -> 463 | erlang:error({badarg, "Expected measurement to be memory, total_run_queue_lengths, {process_info, list()}, or a {module(), function(), list()} tuple"}, [Term]). 464 | 465 | -spec make_measurements_and_filter_misbehaving([measurement()]) -> [measurement()]. 466 | make_measurements_and_filter_misbehaving(Measurements) -> 467 | [Measurement || Measurement <- Measurements, make_measurement(Measurement) =/= error]. 468 | 469 | -spec make_measurement(measurement()) -> measurement() | no_return(). 470 | make_measurement(Measurement = {M, F, A}) -> 471 | try erlang:apply(M, F, A) of 472 | _ -> Measurement 473 | catch 474 | Class:Reason:Stacktrace -> 475 | ?LOG_ERROR("Error when calling MFA defined by measurement: ~p ~p ~p~n" 476 | "Class=~p~nReason=~p~nStacktrace=~p~n", 477 | [M, F, A, Class, Reason, Stacktrace]), 478 | error 479 | end. 480 | 481 | ?DOC(false). 482 | handle_call(get_measurements, _From, State = #{measurements := Measurements}) -> 483 | {reply, Measurements, State}; 484 | handle_call(_Request, _From, State) -> 485 | {reply, ok, State}. 486 | 487 | ?DOC(false). 488 | handle_cast(_Msg, State) -> {noreply, State}. 489 | 490 | ?DOC(false). 491 | handle_info(collect, State) -> 492 | GoodMeasurements = make_measurements_and_filter_misbehaving(maps:get(measurements, State)), 493 | schedule_measurement(maps:get(period, State)), 494 | {noreply, State#{measurements := GoodMeasurements}}; 495 | handle_info(_, State) -> 496 | {noreply, State}. 497 | 498 | ?DOC(false). 499 | terminate(_Reason, _State) -> ok. 500 | 501 | ?DOC(false). 502 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 503 | -------------------------------------------------------------------------------- /src/telemetry_poller_app.erl: -------------------------------------------------------------------------------- 1 | %% @private 2 | -module(telemetry_poller_app). 3 | 4 | -behaviour(application). 5 | 6 | -export([start/2, stop/1]). 7 | 8 | start(_StartType, _StartArgs) -> 9 | PollerChildSpec = 10 | case application:get_env(telemetry_poller, default, []) of 11 | false -> 12 | []; 13 | PollerOpts -> 14 | Default = #{ 15 | name => telemetry_poller_default, 16 | measurements => [ 17 | memory, 18 | total_run_queue_lengths, 19 | system_counts, 20 | persistent_term 21 | ] 22 | }, 23 | FinalOpts = maps:to_list(maps:merge(Default, maps:from_list(PollerOpts))), 24 | [telemetry_poller:child_spec(FinalOpts)] 25 | end, 26 | telemetry_poller_sup:start_link(PollerChildSpec). 27 | 28 | stop(_State) -> 29 | ok. 30 | -------------------------------------------------------------------------------- /src/telemetry_poller_builtin.erl: -------------------------------------------------------------------------------- 1 | %% @private 2 | -module(telemetry_poller_builtin). 3 | 4 | -export([ 5 | memory/0, 6 | total_run_queue_lengths/0, 7 | system_counts/0, 8 | persistent_term/0, 9 | process_info/3 10 | ]). 11 | 12 | -spec process_info([atom()], atom(), [atom()]) -> ok. 13 | process_info(Event, Name, Measurements) -> 14 | case erlang:whereis(Name) of 15 | undefined -> ok; 16 | Pid -> 17 | case erlang:process_info(Pid, Measurements) of 18 | undefined -> ok; 19 | Info -> telemetry:execute(Event, maps:from_list(Info), #{name => Name}) 20 | end 21 | end. 22 | 23 | -spec memory() -> ok. 24 | memory() -> 25 | Measurements = erlang:memory(), 26 | telemetry:execute([vm, memory], maps:from_list(Measurements), #{}). 27 | 28 | -spec total_run_queue_lengths() -> ok. 29 | total_run_queue_lengths() -> 30 | Total = cpu_stats(total), 31 | CPU = cpu_stats(cpu), 32 | telemetry:execute([vm, total_run_queue_lengths], #{ 33 | total => Total, 34 | cpu => CPU, 35 | io => Total - CPU}, 36 | #{}). 37 | 38 | -spec cpu_stats(total | cpu) -> non_neg_integer(). 39 | cpu_stats(total) -> 40 | erlang:statistics(total_run_queue_lengths_all); 41 | cpu_stats(cpu) -> 42 | erlang:statistics(total_run_queue_lengths). 43 | 44 | -spec system_counts() -> ok. 45 | system_counts() -> 46 | ProcessCount = erlang:system_info(process_count), 47 | AtomCount = erlang:system_info(atom_count), 48 | PortCount = erlang:system_info(port_count), 49 | telemetry:execute([vm, system_counts], #{ 50 | process_count => ProcessCount, 51 | atom_count => AtomCount, 52 | port_count => PortCount 53 | }). 54 | 55 | -spec persistent_term() -> ok. 56 | persistent_term() -> 57 | Info = persistent_term:info(), 58 | telemetry:execute([vm, persistent_term], Info, #{}). 59 | -------------------------------------------------------------------------------- /src/telemetry_poller_sup.erl: -------------------------------------------------------------------------------- 1 | %% @private 2 | -module(telemetry_poller_sup). 3 | 4 | -behaviour(supervisor). 5 | 6 | -export([start_link/1]). 7 | -export([init/1]). 8 | 9 | -define(SERVER, ?MODULE). 10 | 11 | start_link(PollerChildSpec) -> 12 | supervisor:start_link({local, ?SERVER}, ?MODULE, PollerChildSpec). 13 | 14 | init(PollerChildSpec) -> 15 | SupFlags = #{strategy => one_for_one, 16 | intensity => 1, 17 | period => 5}, 18 | {ok, {SupFlags, PollerChildSpec}}. 19 | -------------------------------------------------------------------------------- /test/support/test_handler.erl: -------------------------------------------------------------------------------- 1 | -module(test_handler). 2 | 3 | -export([echo_event/4]). 4 | 5 | echo_event(Event, Measurements, Metadata, Config) -> 6 | erlang:send(maps:get(caller, Config), {event, Event, Measurements, Metadata}). -------------------------------------------------------------------------------- /test/support/test_measure.erl: -------------------------------------------------------------------------------- 1 | -module(test_measure). 2 | 3 | -export([single_sample/3, raise/0]). 4 | 5 | single_sample(Event, Measures, Metadata) -> 6 | telemetry:execute(Event, Measures, Metadata). 7 | 8 | raise() -> 9 | erlang:raise(error, "I'm raising because I can!", [{?MODULE, raise, 0}]). -------------------------------------------------------------------------------- /test/telemetry_poller_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(telemetry_poller_SUITE). 2 | 3 | -compile(export_all). 4 | 5 | -include_lib("common_test/include/ct.hrl"). 6 | -include_lib("stdlib/include/assert.hrl"). 7 | 8 | all() -> [ 9 | accepts_name_opt, 10 | accepts_global_name_opt, 11 | dont_start_when_default_false, 12 | can_configure_sampling_period, 13 | dispatches_custom_mfa, 14 | dispatches_memory, 15 | dispatches_persistent_term, 16 | dispatches_process_info, 17 | dispatches_system_counts, 18 | dispatches_total_run_queue_lengths, 19 | doesnt_start_given_invalid_measurements, 20 | doesnt_start_given_invalid_period, 21 | doesnt_start_given_invalid_init_delay, 22 | measurements_can_be_listed, 23 | measurement_removed_if_it_raises, 24 | multiple_unnamed 25 | ]. 26 | 27 | init_per_suite(Config) -> 28 | {ok, _} = application:ensure_all_started(telemetry_poller), 29 | Config. 30 | 31 | end_per_suite(_Config) -> 32 | application:stop(telemetry_poller). 33 | 34 | dont_start_when_default_false(_Config) -> 35 | application:set_env(telemetry_poller, default, false), 36 | true = erlang:is_pid(erlang:whereis(telemetry_poller_default)), 37 | ok = application:stop(telemetry_poller), 38 | {ok, _} = application:ensure_all_started(telemetry_poller), 39 | undefined = erlang:whereis(telemetry_poller_default). 40 | 41 | accepts_name_opt(_Config) -> 42 | Name = my_poller, 43 | {ok, Pid} = telemetry_poller:start_link([{name, Name}]), 44 | FoundPid = erlang:whereis(Name), 45 | FoundPid = Pid. 46 | 47 | accepts_global_name_opt(_Config) -> 48 | Name = my_poller, 49 | {ok, Pid} = telemetry_poller:start_link([{name, {global, Name}}]), 50 | FoundPid = global:whereis_name(Name), 51 | FoundPid = Pid. 52 | 53 | multiple_unnamed(_Config) -> 54 | {ok, _} = telemetry_poller:start_link([]), 55 | {ok, _} = telemetry_poller:start_link([]), 56 | ok. 57 | 58 | can_configure_sampling_period(_Config) -> 59 | Period = 500, 60 | {ok, Pid} = telemetry_poller:start_link([{measurements, []}, {period, Period}, {init_delay, 0}]), 61 | State = sys:get_state(Pid), 62 | Period = maps:get(period, State). 63 | 64 | doesnt_start_given_invalid_period(_Config) -> 65 | ?assertError({badarg, "Expected period to be a positive integer"}, telemetry_poller:start_link([{measurements, []}, {period, "1"}])). 66 | 67 | doesnt_start_given_invalid_init_delay(_Config) -> 68 | ?assertError({badarg, "Expected init_delay to be 0 or a positive integer"}, 69 | telemetry_poller:start_link([{measurements, []}, {init_delay, "1"}])). 70 | 71 | doesnt_start_given_invalid_measurements(_Config) -> 72 | ?assertError({badarg, "Expected measurement " ++ _}, telemetry_poller:start_link([{measurements, [invalid_measurement]}])), 73 | ?assertError({badarg, "Expected measurements to be a list"}, telemetry_poller:start_link([{measurements, {}}])). 74 | 75 | measurements_can_be_listed(_Config) -> 76 | Measurement1 = {telemetry_poller_builtin, memory, []}, 77 | Measurement2 = {test_measure, single_sample, [{a, second, test, event}, #{sample => 1}, #{}]}, 78 | {ok, Poller} = telemetry_poller:start_link([{measurements, [memory, Measurement2]},{period, 100}]), 79 | ?assertMatch([Measurement1, Measurement2], telemetry_poller:list_measurements(Poller)). 80 | 81 | measurement_removed_if_it_raises(_Config) -> 82 | InvalidMeasurement = {test_measure, raise, []}, 83 | {ok, Poller} = telemetry_poller:start_link([{measurements, [InvalidMeasurement]},{period, 100}]), 84 | ct:sleep(200), 85 | ?assert([] =:= telemetry_poller:list_measurements(Poller)). 86 | 87 | dispatches_custom_mfa(_Config) -> 88 | Event = [a, test, event], 89 | Measurements = #{sample => 1}, 90 | Metadata = #{some => "metadata"}, 91 | Measurement = {test_measure, single_sample, [Event, Measurements, Metadata]}, 92 | HandlerId = attach_to(Event), 93 | {ok, _Pid} = telemetry_poller:start_link([{measurements, [Measurement]},{period, 100}]), 94 | receive 95 | {event, Event, Measurements, Metadata} -> 96 | ok 97 | after 98 | 1000 -> 99 | ct:fail(timeout_receive_echo) 100 | end, 101 | telemetry:detach(HandlerId). 102 | 103 | dispatches_memory(_Config) -> 104 | {ok, _Poller} = telemetry_poller:start_link([{measurements, [memory]},{period, 100}]), 105 | HandlerId = attach_to([vm, memory]), 106 | receive 107 | {event, [vm, memory], #{total := _}, _} -> 108 | telemetry:detach(HandlerId), 109 | ?assert(true) 110 | after 111 | 1000 -> 112 | ct:fail(timeout_receive_echo) 113 | end. 114 | 115 | dispatches_total_run_queue_lengths(_Config) -> 116 | {ok, _Poller} = telemetry_poller:start_link([{measurements, [total_run_queue_lengths]},{period, 100}]), 117 | HandlerId = attach_to([vm, total_run_queue_lengths]), 118 | receive 119 | {event, [vm, total_run_queue_lengths], #{total := _, cpu := _, io := _}, _} -> 120 | telemetry:detach(HandlerId), 121 | ?assert(true) 122 | after 123 | 1000 -> 124 | ct:fail(timeout_receive_echo) 125 | end. 126 | 127 | dispatches_system_counts(_Config) -> 128 | {ok, _Poller} = telemetry_poller:start_link([{measurements, [system_counts]},{period, 100}]), 129 | HandlerId = attach_to([vm, system_counts]), 130 | receive 131 | {event, [vm, system_counts], #{process_count := _, atom_count := _, port_count := _}, _} -> 132 | telemetry:detach(HandlerId), 133 | ?assert(true) 134 | after 135 | 1000 -> 136 | ct:fail(timeout_receive_echo) 137 | end. 138 | 139 | dispatches_process_info(_Config) -> 140 | ProcessInfo = [{name, user}, {event, [my_app, user]}, {keys, [memory, message_queue_len]}], 141 | {ok, _Poller} = telemetry_poller:start_link([{measurements, [{process_info, ProcessInfo}]},{period, 100}]), 142 | HandlerId = attach_to([my_app, user]), 143 | receive 144 | {event, [my_app, user], #{memory := _, message_queue_len := _}, #{name := user}} -> 145 | telemetry:detach(HandlerId), 146 | ?assert(true) 147 | after 148 | 1000 -> 149 | ct:fail(timeout_receive_echo) 150 | end. 151 | 152 | dispatches_persistent_term(_Config) -> 153 | {ok, _Poller} = telemetry_poller:start_link([{measurements, [persistent_term]}, {period, 100}]), 154 | HandlerId = attach_to([vm, persistent_term]), 155 | receive 156 | {event, [vm, persistent_term], #{count := Count, memory := Memory}, _} -> 157 | ?assert(is_integer(Count)), 158 | ?assert(Count >= 0), 159 | ?assert(is_integer(Memory)), 160 | ?assert(Memory >= 0), 161 | telemetry:detach(HandlerId), 162 | ?assert(true) 163 | after 164 | 1000 -> 165 | ct:fail(timeout_receive_echo) 166 | end. 167 | 168 | attach_to(Event) -> 169 | HandlerId = make_ref(), 170 | telemetry:attach(HandlerId, Event, fun test_handler:echo_event/4, #{caller => erlang:self()}), 171 | HandlerId. 172 | --------------------------------------------------------------------------------