├── .formatter.exs ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── logger_backends.ex └── logger_backends │ ├── application.ex │ ├── config.ex │ ├── console.ex │ ├── handler.ex │ ├── supervisor.ex │ └── watcher.ex ├── mix.exs ├── mix.lock └── test ├── logger_backends ├── console_test.exs └── handler_test.exs ├── logger_backends_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: Test (Elixir ${{matrix.elixir}} | Erlang/OTP ${{matrix.otp}}) 12 | runs-on: ubuntu-20.04 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - otp: '25' 18 | elixir: main 19 | lint: true 20 | env: 21 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 22 | MIX_ENV: test 23 | 24 | steps: 25 | - name: Clone repository 26 | uses: actions/checkout@v1 27 | 28 | - name: Install OTP and Elixir 29 | uses: erlef/setup-beam@v1 30 | with: 31 | otp-version: ${{ matrix.otp }} 32 | elixir-version: ${{ matrix.elixir }} 33 | version-type: 'strict' 34 | 35 | - name: Install dependencies 36 | run: mix do deps.get --only test, deps.compile 37 | 38 | - name: Check for formatted code 39 | if: ${{ matrix.lint }} 40 | run: mix format --check-formatted 41 | 42 | - name: Check for unused dependencies 43 | if: ${{ matrix.lint }} 44 | run: mix do deps.get, deps.unlock --check-unused 45 | 46 | - name: Check for compilation warnings 47 | if: ${{ matrix.lint }} 48 | run: mix compile --warnings-as-errors 49 | 50 | - name: Run tests 51 | run: mix test --trace 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | logger_backends-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LoggerBackends 2 | 3 | [![hex.pm badge](https://img.shields.io/badge/Package%20on%20hex.pm-informational)](https://hex.pm/packages/logger_backends) 4 | [![Documentation badge](https://img.shields.io/badge/Documentation-ff69b4)][docs] 5 | [![CI](https://github.com/elixir-lang/logger_backends/actions/workflows/main.yml/badge.svg)](https://github.com/elixir-lang/logger_backends/actions/workflows/main.yml) 6 | 7 | Logger backends functionality for Elixir v1.15+. 8 | 9 | [Read the docs to learn more][docs]. 10 | 11 | ## Installation 12 | 13 | The package can be installed by adding `logger_backends` to your list of dependencies in `mix.exs`: 14 | 15 | ```elixir 16 | def deps do 17 | [ 18 | {:logger_backends, "~> 1.0.0"} 19 | ] 20 | end 21 | ``` 22 | 23 | ## License 24 | 25 | Copyright 2023 Elixir Team. 26 | 27 | [Apache License 2.0](LICENSE). 28 | 29 | [docs]: https://hexdocs.pm/logger_backends 30 | -------------------------------------------------------------------------------- /lib/logger_backends.ex: -------------------------------------------------------------------------------- 1 | defmodule LoggerBackends do 2 | @moduledoc """ 3 | `:gen_event`-based logger handlers with overload protection. 4 | 5 | This module provides backends for Elixir's Logger with 6 | built-in overload protection. This was the default 7 | mechanism for hooking into Elixir's Logger until Elixir v1.15. 8 | 9 | Elixir backends run in a single separate process which comes with 10 | overload protection. All backends run in this same process as a 11 | unified front for handling log events. 12 | 13 | The available backends by default are: 14 | 15 | * `LoggerBackends.Console` - logs messages to the console 16 | (see its documentation for more information) 17 | 18 | Developers may also implement their own backends, an option that 19 | is explored in more detail later. 20 | 21 | Backends can be added and removed via `add/2` and `remove/2` functions. 22 | This is often done in your `c:Application.start/2` callback: 23 | 24 | @impl true 25 | def start(_type, _args) do 26 | LoggerBackends.add(MyCustomBackend) 27 | 28 | # ... 29 | end 30 | 31 | The backend can be configured in your config files: 32 | 33 | config :logger, MyCustomBackend, 34 | some_config: ... 35 | 36 | ## Application configuration 37 | 38 | Application configuration goes under the `:logger` application for 39 | backwards compatibility. The following keys must be set before 40 | the `:logger` application (and this application) are started. 41 | 42 | * `:discard_threshold_periodic_check` - a periodic check that 43 | checks and reports if logger is discarding messages. It logs a warning 44 | message whenever the system is (or continues) in discard mode and 45 | it logs a warning message whenever if the system was discarding messages 46 | but stopped doing so after the previous check. By default it runs 47 | every `30_000` milliseconds. 48 | 49 | * `:start_options` - passes start options to LoggerBackends's main process, such 50 | as `:spawn_opt` and `:hibernate_after`. All options in `t:GenServer.option/0` 51 | are accepted, except `:name`. 52 | 53 | ## Runtime configuration 54 | 55 | The following keys can be set at runtime via the `configure/1` function. 56 | In your config files, they also go under the `:logger` application 57 | for backwards compatibility. 58 | 59 | * `:utc_log` - when `true`, uses UTC in logs. By default it uses 60 | local time (i.e., it defaults to `false`). 61 | 62 | * `:truncate` - the maximum message size to be logged (in bytes). 63 | Defaults to 8192 bytes. Note this configuration is approximate. 64 | Truncated messages will have `" (truncated)"` at the end. 65 | The atom `:infinity` can be passed to disable this behavior. 66 | 67 | * `:sync_threshold` - if the `Logger` manager has more than 68 | `:sync_threshold` messages in its queue, `Logger` will change 69 | to *sync mode*, to apply backpressure to the clients. 70 | `Logger` will return to *async mode* once the number of messages 71 | in the queue is reduced to one below the `sync_threshold`. 72 | Defaults to 20 messages. `:sync_threshold` can be set to `0` to 73 | force *sync mode*. 74 | 75 | * `:discard_threshold` - if the `Logger` manager has more than 76 | `:discard_threshold` messages in its queue, `Logger` will change 77 | to *discard mode* and messages will be discarded directly in the 78 | clients. `Logger` will return to *sync mode* once the number of 79 | messages in the queue is reduced to one below the `discard_threshold`. 80 | Defaults to 500 messages. 81 | 82 | ## Custom backends 83 | 84 | Any developer can create their own backend. Since `Logger` is an 85 | event manager powered by `:gen_event`, writing a new backend 86 | is a matter of creating an event handler, as described in the 87 | [`:gen_event`](`:gen_event`) documentation. 88 | 89 | From now on, we will be using the term "event handler" to refer 90 | to your custom backend, as we head into implementation details. 91 | 92 | The event manager and all added event handlers are automatically 93 | supervised by `Logger`. If a backend fails to start by returning 94 | `{:error, :ignore}` from its `init/1` callback, then it's not added 95 | to the backends but nothing fails. If a backend fails to start by 96 | returning `{:error, reason}` from its `init/1` callback, the system 97 | will fail to start. 98 | 99 | Once initialized, the handler should be designed to handle the 100 | following events: 101 | 102 | * `{level, group_leader, {Logger, message, timestamp, metadata}}` where: 103 | * `level` is one of `:debug`, `:info`, `:warn`, or `:error`, as previously 104 | described (for compatibility with pre 1.10 backends the `:notice` will 105 | be translated to `:info` and all messages above `:error` will be translated 106 | to `:error`) 107 | * `group_leader` is the group leader of the process which logged the message 108 | * `{Logger, message, timestamp, metadata}` is a tuple containing information 109 | about the logged message: 110 | * the first element is always the atom `Logger` 111 | * `message` is the actual message (as chardata) 112 | * `timestamp` is the timestamp for when the message was logged, as a 113 | `{{year, month, day}, {hour, minute, second, millisecond}}` tuple 114 | * `metadata` is a keyword list of metadata used when logging the message 115 | 116 | * `:flush` 117 | 118 | It is recommended that handlers ignore messages where the group 119 | leader is in a different node than the one where the handler is 120 | installed. For example: 121 | 122 | def handle_event({_level, gl, {Logger, _, _, _}}, state) 123 | when node(gl) != node() do 124 | {:ok, state} 125 | end 126 | 127 | In the case of the event `:flush` handlers should flush any pending 128 | data. This event is triggered by `Logger.flush/0`. 129 | 130 | Furthermore, backends can be configured via the `configure_backend/2` 131 | function which requires event handlers to handle calls of the 132 | following format: 133 | 134 | {:configure, options} 135 | 136 | where `options` is a keyword list. The result of the call is the result 137 | returned by `configure_backend/2`. The recommended return value for 138 | successful configuration is `:ok`. For example: 139 | 140 | def handle_call({:configure, options}, state) do 141 | new_state = reconfigure_state(state, options) 142 | {:ok, :ok, new_state} 143 | end 144 | 145 | It is recommended that backends support at least the following configuration 146 | options: 147 | 148 | * `:level` - the logging level for that backend 149 | * `:format` - the logging format for that backend 150 | * `:metadata` - the metadata to include in that backend 151 | 152 | Check the `LoggerBackends.Console` implementation in Elixir's codebase 153 | for examples on how to handle the recommendations in this section and 154 | how to process the existing options. 155 | 156 | ## Levels 157 | 158 | For backwards compatibility purposes, a Logger Backend receives only `:debug`, 159 | `:info`, `:warn`, and `:error` as log levels. In particular, notice you will 160 | need to convert `:warn` to `:warning` if you plan to use the `Logger` functions. 161 | """ 162 | 163 | @typedoc """ 164 | A logger handler. 165 | """ 166 | @typedoc since: "1.0.0" 167 | @type backend :: :gen_event.handler() 168 | 169 | @doc """ 170 | Applies runtime configuration to all backends. 171 | 172 | See the module doc for more information. 173 | """ 174 | @backend_options [:sync_threshold, :discard_threshold, :truncate, :utc_log] 175 | @spec configure(keyword) :: :ok 176 | def configure(options) do 177 | LoggerBackends.Config.configure(Keyword.take(options, @backend_options)) 178 | :ok = :logger.update_handler_config(LoggerBackends, :config, :refresh) 179 | end 180 | 181 | @doc """ 182 | Configures a given backend. 183 | """ 184 | @spec configure(backend, keyword) :: term 185 | def configure(backend, options) when is_list(options) do 186 | :gen_event.call(LoggerBackends, backend, {:configure, options}) 187 | end 188 | 189 | @doc """ 190 | Adds a new backend. 191 | 192 | Adding a backend calls the `init/1` function in that backend 193 | with the name of the backend as its argument. For example, 194 | calling 195 | 196 | LoggerBackends.add(MyBackend) 197 | 198 | will call `MyBackend.init(MyBackend)` to initialize the new 199 | backend. If the backend's `init/1` callback returns `{:ok, _}`, 200 | then this function returns `{:ok, pid}`. If the handler returns 201 | `{:error, :ignore}` from `init/1`, this function still returns 202 | `{:ok, pid}` but the handler is not started. If the handler 203 | returns `{:error, reason}` from `init/1`, this function returns 204 | `{:error, {reason, info}}` where `info` is more information on 205 | the backend that failed to start. 206 | 207 | ## Options 208 | 209 | * `:flush` - when `true`, guarantees all messages currently sent 210 | to `Logger` are processed before the backend is added 211 | 212 | """ 213 | @spec add(backend, keyword) :: Supervisor.on_start_child() 214 | def add(backend, opts \\ []) do 215 | _ = if opts[:flush], do: Logger.flush() 216 | 217 | case LoggerBackends.Supervisor.add(backend) do 218 | {:ok, _} = ok -> 219 | ok 220 | 221 | {:error, {:already_started, _pid}} -> 222 | {:error, :already_present} 223 | 224 | {:error, _} = error -> 225 | error 226 | end 227 | end 228 | 229 | @doc """ 230 | Removes a backend. 231 | 232 | ## Options 233 | 234 | * `:flush` - when `true`, guarantees all messages currently sent 235 | to `Logger` are processed before the backend is removed 236 | 237 | """ 238 | @spec remove(backend, keyword) :: :ok | {:error, term} 239 | def remove(backend, opts \\ []) do 240 | _ = if opts[:flush], do: Logger.flush() 241 | LoggerBackends.Supervisor.remove(backend) 242 | end 243 | end 244 | -------------------------------------------------------------------------------- /lib/logger_backends/application.ex: -------------------------------------------------------------------------------- 1 | defmodule LoggerBackends.Application do 2 | @moduledoc false 3 | use Application 4 | 5 | @impl true 6 | def start(_type, _args) do 7 | start_options = Application.fetch_env!(:logger, :start_options) 8 | counter = :counters.new(1, [:atomics]) 9 | 10 | children = [ 11 | %{ 12 | id: :gen_event, 13 | start: {:gen_event, :start_link, [{:local, LoggerBackends}, start_options]}, 14 | modules: :dynamic 15 | }, 16 | {LoggerBackends.Watcher, {LoggerBackends.Config, counter}}, 17 | LoggerBackends.Supervisor 18 | ] 19 | 20 | with {:ok, pid} <- Supervisor.start_link(children, strategy: :rest_for_one) do 21 | :ok = 22 | :logger.add_handler(LoggerBackends, LoggerBackends.Handler, %{ 23 | level: :all, 24 | config: %{counter: counter}, 25 | filter_default: :log, 26 | filters: [] 27 | }) 28 | 29 | {:ok, pid} 30 | end 31 | end 32 | 33 | @impl true 34 | def stop(_) do 35 | _ = :logger.remove_handler(LoggerBackends) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/logger_backends/config.ex: -------------------------------------------------------------------------------- 1 | defmodule LoggerBackends.Config do 2 | @moduledoc false 3 | 4 | @behaviour :gen_event 5 | @name __MODULE__ 6 | @update_counter_message {__MODULE__, :update_counter} 7 | 8 | def configure(options) do 9 | :gen_event.call(LoggerBackends, @name, {:configure, options}) 10 | end 11 | 12 | ## Callbacks 13 | 14 | def init(counter) do 15 | state = load_state(counter) 16 | state = update_counter(state, false) 17 | schedule_update_counter(state) 18 | {:ok, state} 19 | end 20 | 21 | defp load_state(counter) do 22 | {counter, :log, Application.fetch_env!(:logger, :discard_threshold), 23 | Application.fetch_env!(:logger, :discard_threshold_periodic_check)} 24 | end 25 | 26 | def handle_event(_event, state) do 27 | {:ok, update_counter(state, false)} 28 | end 29 | 30 | def handle_call({:configure, options}, {counter, _, _, _}) do 31 | Enum.each(options, fn {key, value} -> 32 | Application.put_env(:logger, key, value) 33 | end) 34 | 35 | {:ok, :ok, load_state(counter)} 36 | end 37 | 38 | def handle_info(@update_counter_message, state) do 39 | state = update_counter(state, true) 40 | schedule_update_counter(state) 41 | {:ok, state} 42 | end 43 | 44 | def handle_info(_, state) do 45 | {:ok, state} 46 | end 47 | 48 | def terminate(_reason, _state) do 49 | :ok 50 | end 51 | 52 | def code_change(_old, state, _extra) do 53 | {:ok, state} 54 | end 55 | 56 | @counter_pos 1 57 | 58 | defp update_counter({counter, log, discard_threshold, discard_period}, periodic_check?) do 59 | # If length is more than the total, it means the counter is behind, 60 | # due to non-log messages, so we need to increase the counter. 61 | # 62 | # If length is less than the total, then we either have a spike or 63 | # the counter drifted due to failures. 64 | # 65 | # Because we always bump the counter and then we send the message, 66 | # there is a chance clients have bumped the counter but they did not 67 | # deliver the message yet. Those bumps will be lost. At the same time, 68 | # we are careful to read the counter first here, so if the counter is 69 | # bumped after we read from it, those bumps won't be lost. 70 | total = :counters.get(counter, @counter_pos) 71 | {:message_queue_len, length} = Process.info(self(), :message_queue_len) 72 | :counters.add(counter, @counter_pos, length - total) 73 | 74 | # In case we are logging but we reached the threshold, we log that we 75 | # started discarding messages. This can only be reverted by the periodic 76 | # discard check. 77 | cond do 78 | total >= discard_threshold -> 79 | if log == :log or periodic_check? do 80 | warn("Attempted to log #{total} messages, which is above :discard_threshold") 81 | end 82 | 83 | {counter, :discard, discard_threshold, discard_period} 84 | 85 | log == :discard and periodic_check? -> 86 | warn("Attempted to log #{total} messages, which is below :discard_threshold") 87 | {counter, :log, discard_threshold, discard_period} 88 | 89 | true -> 90 | {counter, log, discard_threshold, discard_period} 91 | end 92 | end 93 | 94 | defp warn(message) do 95 | system_time = :os.system_time(:microsecond) 96 | utc_log = Application.fetch_env!(:logger, :utc_log) 97 | date_time_ms = Logger.Formatter.system_time_to_date_time_ms(system_time, utc_log) 98 | event = {Logger, message, date_time_ms, pid: self()} 99 | :gen_event.notify(self(), {:warning, Process.group_leader(), event}) 100 | end 101 | 102 | defp schedule_update_counter({_, _, _, discard_period}) do 103 | Process.send_after(self(), @update_counter_message, discard_period) 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/logger_backends/console.ex: -------------------------------------------------------------------------------- 1 | defmodule LoggerBackends.Console do 2 | @moduledoc ~S""" 3 | A logger backend that logs messages by printing them to the console. 4 | 5 | ## Options 6 | 7 | * `:level` - the level to be logged by this backend. 8 | Note that messages are filtered by the general 9 | `:level` configuration for the `:logger` application first. 10 | 11 | * `:format` - the format message used to print logs. 12 | Defaults to: `"\n$time $metadata[$level] $message\n"`. 13 | It may also be a `{module, function}` tuple that is invoked 14 | with the log level, the message, the current timestamp and 15 | the metadata and must return `t:IO.chardata/0`. See 16 | `Logger.Formatter`. 17 | 18 | * `:metadata` - the metadata to be printed by `$metadata`. 19 | Defaults to an empty list (no metadata). 20 | Setting `:metadata` to `:all` prints all metadata. See 21 | the "Metadata" section in the `Logger` documentation for 22 | more information. 23 | 24 | * `:colors` - a keyword list of coloring options. 25 | 26 | * `:device` - the device to log error messages to. Defaults to 27 | `:user` but can be changed to something else such as `:standard_error`. 28 | 29 | * `:max_buffer` - maximum events to buffer while waiting 30 | for a confirmation from the IO device (default: 32). 31 | Once the buffer is full, the backend will block until 32 | a confirmation is received. 33 | 34 | The supported keys in the `:colors` keyword list are: 35 | 36 | * `:enabled` - boolean value that allows for switching the 37 | coloring on and off. Defaults to: `IO.ANSI.enabled?/0` 38 | 39 | * `:debug` - color for debug messages. Defaults to: `:cyan` 40 | 41 | * `:info` - color for info and notice messages. Defaults to: `:normal` 42 | 43 | * `:warning` - color for warning messages. Defaults to: `:yellow` 44 | 45 | * `:error` - color for error and higher messages. Defaults to: `:red` 46 | 47 | See the `IO.ANSI` module for a list of colors and attributes. 48 | 49 | Here is an example of how to configure this backend in a 50 | `config/config.exs` file: 51 | 52 | config :logger, LoggerBackends.Console, 53 | format: "\n$time $metadata[$level] $message\n", 54 | metadata: [:user_id] 55 | 56 | """ 57 | 58 | @behaviour :gen_event 59 | 60 | defstruct buffer: [], 61 | buffer_size: 0, 62 | colors: nil, 63 | device: nil, 64 | format: nil, 65 | level: nil, 66 | max_buffer: nil, 67 | metadata: nil, 68 | output: nil, 69 | ref: nil 70 | 71 | @impl true 72 | def init(atom) when is_atom(atom) do 73 | config = read_env() 74 | device = Keyword.get(config, :device, :user) 75 | 76 | if Process.whereis(device) do 77 | {:ok, init(config, %__MODULE__{})} 78 | else 79 | {:error, :ignore} 80 | end 81 | end 82 | 83 | def init({__MODULE__, opts}) when is_list(opts) do 84 | config = configure_merge(read_env(), opts) 85 | {:ok, init(config, %__MODULE__{})} 86 | end 87 | 88 | @impl true 89 | def handle_call({:configure, options}, state) do 90 | {:ok, :ok, configure(options, state)} 91 | end 92 | 93 | @impl true 94 | def handle_event({level, _gl, {Logger, msg, ts, md}}, state) do 95 | %{level: log_level, ref: ref, buffer_size: buffer_size, max_buffer: max_buffer} = state 96 | 97 | {:erl_level, level} = List.keyfind(md, :erl_level, 0, {:erl_level, level}) 98 | 99 | cond do 100 | not meet_level?(level, log_level) -> 101 | {:ok, state} 102 | 103 | is_nil(ref) -> 104 | {:ok, log_event(level, msg, ts, md, state)} 105 | 106 | buffer_size < max_buffer -> 107 | {:ok, buffer_event(level, msg, ts, md, state)} 108 | 109 | buffer_size === max_buffer -> 110 | state = buffer_event(level, msg, ts, md, state) 111 | {:ok, await_io(state)} 112 | end 113 | end 114 | 115 | def handle_event(:flush, state) do 116 | {:ok, flush(state)} 117 | end 118 | 119 | def handle_event(_, state) do 120 | {:ok, state} 121 | end 122 | 123 | @impl true 124 | def handle_info({:io_reply, ref, msg}, %{ref: ref} = state) do 125 | {:ok, handle_io_reply(msg, state)} 126 | end 127 | 128 | def handle_info({:DOWN, ref, _, pid, reason}, %{ref: ref}) do 129 | raise "device #{inspect(pid)} exited: " <> Exception.format_exit(reason) 130 | end 131 | 132 | def handle_info(_, state) do 133 | {:ok, state} 134 | end 135 | 136 | @impl true 137 | def code_change(_old_vsn, state, _extra) do 138 | {:ok, state} 139 | end 140 | 141 | @impl true 142 | def terminate(_reason, _state) do 143 | :ok 144 | end 145 | 146 | ## Helpers 147 | 148 | defp meet_level?(_lvl, nil), do: true 149 | 150 | defp meet_level?(lvl, min) do 151 | Logger.compare_levels(lvl, min) != :lt 152 | end 153 | 154 | defp configure(options, state) do 155 | config = configure_merge(read_env(), options) 156 | Application.put_env(:logger, __MODULE__, config) 157 | init(config, state) 158 | end 159 | 160 | defp init(config, state) do 161 | level = Keyword.get(config, :level) 162 | device = Keyword.get(config, :device, :user) 163 | format = Logger.Formatter.compile(Keyword.get(config, :format)) 164 | colors = configure_colors(config) 165 | metadata = Keyword.get(config, :metadata, []) |> configure_metadata() 166 | max_buffer = Keyword.get(config, :max_buffer, 32) 167 | 168 | %{ 169 | state 170 | | format: format, 171 | metadata: metadata, 172 | level: level, 173 | colors: colors, 174 | device: device, 175 | max_buffer: max_buffer 176 | } 177 | end 178 | 179 | defp configure_metadata(:all), do: :all 180 | defp configure_metadata(metadata), do: Enum.reverse(metadata) 181 | 182 | defp configure_merge(env, options) do 183 | Keyword.merge(env, options, fn 184 | :colors, v1, v2 -> Keyword.merge(v1, v2) 185 | _, _v1, v2 -> v2 186 | end) 187 | end 188 | 189 | defp configure_colors(config) do 190 | colors = Keyword.get(config, :colors, []) 191 | 192 | %{ 193 | emergency: Keyword.get(colors, :error, :red), 194 | alert: Keyword.get(colors, :error, :red), 195 | critical: Keyword.get(colors, :error, :red), 196 | error: Keyword.get(colors, :error, :red), 197 | warning: Keyword.get(colors, :warning, :yellow), 198 | notice: Keyword.get(colors, :info, :normal), 199 | info: Keyword.get(colors, :info, :normal), 200 | debug: Keyword.get(colors, :debug, :cyan), 201 | enabled: Keyword.get(colors, :enabled, IO.ANSI.enabled?()) 202 | } 203 | end 204 | 205 | defp log_event(level, msg, ts, md, %{device: device} = state) do 206 | output = format_event(level, msg, ts, md, state) 207 | %{state | ref: async_io(device, output), output: output} 208 | end 209 | 210 | defp buffer_event(level, msg, ts, md, state) do 211 | %{buffer: buffer, buffer_size: buffer_size} = state 212 | buffer = [buffer | format_event(level, msg, ts, md, state)] 213 | %{state | buffer: buffer, buffer_size: buffer_size + 1} 214 | end 215 | 216 | defp async_io(name, output) when is_atom(name) do 217 | case Process.whereis(name) do 218 | device when is_pid(device) -> 219 | async_io(device, output) 220 | 221 | nil -> 222 | raise "no device registered with the name #{inspect(name)}" 223 | end 224 | end 225 | 226 | defp async_io(device, output) when is_pid(device) do 227 | ref = Process.monitor(device) 228 | send(device, {:io_request, self(), ref, {:put_chars, :unicode, output}}) 229 | ref 230 | end 231 | 232 | defp await_io(%{ref: nil} = state), do: state 233 | 234 | defp await_io(%{ref: ref} = state) do 235 | receive do 236 | {:io_reply, ^ref, :ok} -> 237 | handle_io_reply(:ok, state) 238 | 239 | {:io_reply, ^ref, error} -> 240 | handle_io_reply(error, state) 241 | |> await_io() 242 | 243 | {:DOWN, ^ref, _, pid, reason} -> 244 | raise "device #{inspect(pid)} exited: " <> Exception.format_exit(reason) 245 | end 246 | end 247 | 248 | defp format_event(level, msg, ts, md, state) do 249 | %{format: format, metadata: keys, colors: colors} = state 250 | 251 | format 252 | |> Logger.Formatter.format(level, msg, ts, take_metadata(md, keys)) 253 | |> color_event(level, colors, md) 254 | end 255 | 256 | defp take_metadata(metadata, :all) do 257 | metadata 258 | end 259 | 260 | defp take_metadata(metadata, keys) do 261 | Enum.reduce(keys, [], fn key, acc -> 262 | case Keyword.fetch(metadata, key) do 263 | {:ok, val} -> [{key, val} | acc] 264 | :error -> acc 265 | end 266 | end) 267 | end 268 | 269 | defp color_event(data, _level, %{enabled: false}, _md), do: data 270 | 271 | defp color_event(data, level, %{enabled: true} = colors, md) do 272 | color = md[:ansi_color] || Map.fetch!(colors, level) 273 | [IO.ANSI.format_fragment(color, true), data | IO.ANSI.reset()] 274 | end 275 | 276 | defp log_buffer(%{buffer_size: 0, buffer: []} = state), do: state 277 | 278 | defp log_buffer(state) do 279 | %{device: device, buffer: buffer} = state 280 | %{state | ref: async_io(device, buffer), buffer: [], buffer_size: 0, output: buffer} 281 | end 282 | 283 | defp handle_io_reply(:ok, %{ref: ref} = state) do 284 | Process.demonitor(ref, [:flush]) 285 | log_buffer(%{state | ref: nil, output: nil}) 286 | end 287 | 288 | defp handle_io_reply({:error, {:put_chars, :unicode, _} = error}, state) do 289 | retry_log(error, state) 290 | end 291 | 292 | defp handle_io_reply({:error, :put_chars}, %{output: output} = state) do 293 | retry_log({:put_chars, :unicode, output}, state) 294 | end 295 | 296 | defp handle_io_reply({:error, {:no_translation, _encoding_from, _encoding_to} = error}, state) do 297 | retry_log(error, state) 298 | end 299 | 300 | defp handle_io_reply({:error, error}, _) do 301 | raise "failure while logging console messages: " <> inspect(error) 302 | end 303 | 304 | defp retry_log(error, %{device: device, ref: ref, output: dirty} = state) do 305 | Process.demonitor(ref, [:flush]) 306 | 307 | try do 308 | :unicode.characters_to_binary(dirty) 309 | rescue 310 | ArgumentError -> 311 | clean = ["failure while trying to log malformed data: ", inspect(dirty), ?\n] 312 | %{state | ref: async_io(device, clean), output: clean} 313 | else 314 | {_, good, bad} -> 315 | clean = [good | Logger.Formatter.prune(bad)] 316 | %{state | ref: async_io(device, clean), output: clean} 317 | 318 | _ -> 319 | # A well behaved IO device should not error on good data 320 | raise "failure while logging consoles messages: " <> inspect(error) 321 | end 322 | end 323 | 324 | defp flush(%{ref: nil} = state), do: state 325 | 326 | defp flush(state) do 327 | state 328 | |> await_io() 329 | |> flush() 330 | end 331 | 332 | defp read_env do 333 | Application.get_env(:logger, __MODULE__, Application.get_env(:logger, :console, [])) 334 | end 335 | end 336 | -------------------------------------------------------------------------------- /lib/logger_backends/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule LoggerBackends.Handler do 2 | @moduledoc false 3 | @internal_keys [:counter] 4 | 5 | def filesync(id) do 6 | :gen_event.sync_notify(id, :flush) 7 | end 8 | 9 | ## Config management 10 | 11 | def adding_handler(config) do 12 | {:ok, update_in(config.config, &Map.merge(default_config(), &1))} 13 | end 14 | 15 | def changing_config(:update, config, %{config: :refresh}) do 16 | {:ok, update_in(config.config, &Map.merge(&1, default_config()))} 17 | end 18 | 19 | def filter_config(%{config: data} = config) do 20 | %{config | config: Map.drop(data, @internal_keys)} 21 | end 22 | 23 | defp default_config do 24 | sync_threshold = Application.fetch_env!(:logger, :sync_threshold) 25 | discard_threshold = Application.fetch_env!(:logger, :discard_threshold) 26 | 27 | %{ 28 | utc_log: Application.fetch_env!(:logger, :utc_log), 29 | truncate: Application.fetch_env!(:logger, :truncate), 30 | thresholds: {sync_threshold, discard_threshold} 31 | } 32 | end 33 | 34 | ## Main logging API 35 | 36 | def log(%{level: erl_level, meta: metadata} = event, %{config: config}) do 37 | case threshold(config) do 38 | :discard -> 39 | :ok 40 | 41 | mode -> 42 | %{gl: gl} = metadata 43 | %{truncate: truncate, utc_log: utc_log?} = config 44 | level = erlang_level_to_elixir_level(erl_level) 45 | message = Logger.Formatter.format_event(event, truncate) 46 | timestamp = Map.get_lazy(metadata, :time, fn -> :os.system_time(:microsecond) end) 47 | date_time_ms = Logger.Formatter.system_time_to_date_time_ms(timestamp, utc_log?) 48 | metadata = [erl_level: erl_level] ++ erlang_metadata_to_elixir_metadata(metadata) 49 | event = {level, gl, {Logger, message, date_time_ms, metadata}} 50 | notify(mode, event) 51 | end 52 | rescue 53 | ArgumentError -> {:error, :noproc} 54 | catch 55 | :exit, reason -> {:error, reason} 56 | end 57 | 58 | defp notify(:sync, msg) do 59 | pid = Process.whereis(LoggerBackends) 60 | 61 | # If we are within the logger process itself, 62 | # we cannot use sync notify as that will deadlock. 63 | if pid == self(), do: :gen_event.notify(pid, msg), else: :gen_event.sync_notify(pid, msg) 64 | end 65 | 66 | defp notify(:async, msg) do 67 | :gen_event.notify(LoggerBackends, msg) 68 | end 69 | 70 | @counter_pos 1 71 | 72 | defp threshold(config) do 73 | %{ 74 | counter: counter, 75 | thresholds: {sync, discard} 76 | } = config 77 | 78 | :counters.add(counter, @counter_pos, 1) 79 | value = :counters.get(counter, @counter_pos) 80 | 81 | cond do 82 | value >= discard -> :discard 83 | value >= sync -> :sync 84 | true -> :async 85 | end 86 | end 87 | 88 | ## Metadata helpers 89 | 90 | defp erlang_metadata_to_elixir_metadata(metadata) do 91 | metadata = 92 | case metadata do 93 | %{mfa: {mod, fun, arity}} -> 94 | Map.merge(%{module: mod, function: form_fa(fun, arity)}, metadata) 95 | 96 | %{} -> 97 | metadata 98 | end 99 | 100 | metadata = 101 | case metadata do 102 | %{file: file} -> %{metadata | file: List.to_string(file)} 103 | %{} -> metadata 104 | end 105 | 106 | Map.to_list(metadata) 107 | rescue 108 | _ -> Map.to_list(metadata) 109 | end 110 | 111 | defp form_fa(fun, arity) do 112 | Atom.to_string(fun) <> "/" <> Integer.to_string(arity) 113 | end 114 | 115 | defp erlang_level_to_elixir_level(:none), do: :error 116 | defp erlang_level_to_elixir_level(:emergency), do: :error 117 | defp erlang_level_to_elixir_level(:alert), do: :error 118 | defp erlang_level_to_elixir_level(:critical), do: :error 119 | defp erlang_level_to_elixir_level(:error), do: :error 120 | defp erlang_level_to_elixir_level(:warning), do: :warn 121 | defp erlang_level_to_elixir_level(:notice), do: :info 122 | defp erlang_level_to_elixir_level(:info), do: :info 123 | defp erlang_level_to_elixir_level(:debug), do: :debug 124 | defp erlang_level_to_elixir_level(:all), do: :debug 125 | end 126 | -------------------------------------------------------------------------------- /lib/logger_backends/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule LoggerBackends.Supervisor do 2 | @moduledoc false 3 | use Supervisor 4 | @name __MODULE__ 5 | 6 | def add(backend) do 7 | spec = %{ 8 | id: backend, 9 | start: {LoggerBackends.Watcher, :start_link, [{backend, backend}]}, 10 | restart: :transient 11 | } 12 | 13 | case Supervisor.start_child(@name, spec) do 14 | {:error, :already_present} -> 15 | _ = Supervisor.delete_child(@name, backend) 16 | add(backend) 17 | 18 | other -> 19 | other 20 | end 21 | end 22 | 23 | def remove(backend) do 24 | case Supervisor.terminate_child(@name, backend) do 25 | :ok -> 26 | _ = Supervisor.delete_child(@name, backend) 27 | :ok 28 | 29 | {:error, _} = error -> 30 | error 31 | end 32 | end 33 | 34 | def start_link(_) do 35 | Supervisor.start_link(__MODULE__, [], name: @name) 36 | end 37 | 38 | @impl true 39 | def init(children) do 40 | Supervisor.init(children, strategy: :one_for_one, max_restarts: 30, max_seconds: 3) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/logger_backends/watcher.ex: -------------------------------------------------------------------------------- 1 | defmodule LoggerBackends.Watcher do 2 | @moduledoc false 3 | 4 | require Logger 5 | use GenServer 6 | 7 | def start_link(tuple) do 8 | GenServer.start_link(__MODULE__, tuple) 9 | end 10 | 11 | ## Callbacks 12 | 13 | @doc false 14 | def init({handler, args}) do 15 | Process.flag(:trap_exit, true) 16 | 17 | case :gen_event.delete_handler(LoggerBackends, handler, :ok) do 18 | {:error, :module_not_found} -> 19 | case :gen_event.add_sup_handler(LoggerBackends, handler, args) do 20 | :ok -> 21 | {:ok, handler} 22 | 23 | {:error, :ignore} -> 24 | # Can't return :ignore as a transient child under a one_for_one. 25 | # Instead return ok and then immediately exit normally - using a fake 26 | # message. 27 | send(self(), {:gen_event_EXIT, handler, :normal}) 28 | {:ok, handler} 29 | 30 | {:error, reason} -> 31 | {:stop, reason} 32 | 33 | {:EXIT, _} = exit -> 34 | {:stop, exit} 35 | end 36 | 37 | _ -> 38 | init({handler, args}) 39 | end 40 | end 41 | 42 | @doc false 43 | def handle_info({:gen_event_EXIT, handler, reason}, handler) 44 | when reason in [:normal, :shutdown] do 45 | {:stop, reason, handler} 46 | end 47 | 48 | def handle_info({:gen_event_EXIT, handler, reason}, handler) do 49 | message = [ 50 | ":gen_event handler ", 51 | inspect(handler), 52 | " installed in Logger terminating\n", 53 | "** (exit) ", 54 | format_exit(reason) 55 | ] 56 | 57 | cond do 58 | logger_has_backends?() -> :ok 59 | true -> IO.puts(:stderr, message) 60 | end 61 | 62 | {:stop, reason, handler} 63 | end 64 | 65 | def handle_info(_msg, state) do 66 | {:noreply, state} 67 | end 68 | 69 | defp logger_has_backends?() do 70 | try do 71 | :gen_event.which_handlers(LoggerBackends) != [LoggerBackends.Config] 72 | catch 73 | _, _ -> false 74 | end 75 | end 76 | 77 | def terminate(_reason, handler) do 78 | # On terminate we remove the handler, this makes the 79 | # process sync, allowing existing messages to be flushed 80 | :gen_event.delete_handler(LoggerBackends, handler, :ok) 81 | :ok 82 | end 83 | 84 | defp format_exit({:EXIT, reason}), do: Exception.format_exit(reason) 85 | defp format_exit(reason), do: Exception.format_exit(reason) 86 | end 87 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LoggerBackends.MixProject do 2 | use Mix.Project 3 | 4 | @version "1.0.0" 5 | @url "https://github.com/elixir-lang/logger_backends" 6 | 7 | def project do 8 | [ 9 | app: :logger_backends, 10 | version: @version, 11 | elixir: "~> 1.15-dev", 12 | start_permanent: Mix.env() == :prod, 13 | deps: deps(), 14 | docs: docs(), 15 | preferred_cli_env: [docs: :docs, "hex.publish": :docs], 16 | 17 | # Hex 18 | description: "Logger backends functionality for Elixir v1.15+", 19 | package: [ 20 | maintainers: ["Elixir Team"], 21 | licenses: ["Apache-2.0"], 22 | links: %{"GitHub" => @url} 23 | ] 24 | ] 25 | end 26 | 27 | def application do 28 | [ 29 | extra_applications: [:logger], 30 | mod: {LoggerBackends.Application, []} 31 | ] 32 | end 33 | 34 | defp docs do 35 | [ 36 | main: "LoggerBackends", 37 | source_ref: "v#{@version}", 38 | source_url: @url 39 | ] 40 | end 41 | 42 | defp deps do 43 | [ 44 | {:ex_doc, "~> 0.28", only: :docs} 45 | ] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, 3 | "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, 4 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, 6 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/logger_backends/console_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LoggerBackends.ConsoleTest do 2 | use Logger.Case 3 | 4 | require Logger 5 | import ExUnit.CaptureIO 6 | 7 | setup_all do 8 | Application.put_env(:logger, :default_handler, false) 9 | Logger.App.stop() 10 | Application.start(:logger) 11 | LoggerBackends.add(LoggerBackends.Console) 12 | 13 | on_exit(fn -> 14 | LoggerBackends.remove(LoggerBackends.Console) 15 | Application.delete_env(:logger, :default_handler) 16 | Logger.App.stop() 17 | Application.start(:logger) 18 | end) 19 | end 20 | 21 | setup do 22 | on_exit(fn -> 23 | :ok = 24 | LoggerBackends.configure( 25 | LoggerBackends.Console, 26 | format: nil, 27 | device: :user, 28 | level: nil, 29 | metadata: [], 30 | colors: [enabled: false] 31 | ) 32 | end) 33 | end 34 | 35 | test "does not start when there is no user" do 36 | :ok = LoggerBackends.remove(LoggerBackends.Console) 37 | user = Process.whereis(:user) 38 | 39 | try do 40 | Process.unregister(:user) 41 | 42 | assert :gen_event.add_handler( 43 | LoggerBackends, 44 | LoggerBackends.Console, 45 | LoggerBackends.Console 46 | ) == 47 | {:error, :ignore} 48 | after 49 | Process.register(user, :user) 50 | end 51 | after 52 | {:ok, _} = LoggerBackends.add(LoggerBackends.Console) 53 | end 54 | 55 | test "may use another device" do 56 | LoggerBackends.configure(LoggerBackends.Console, device: :standard_error) 57 | 58 | assert capture_io(:standard_error, fn -> 59 | Logger.debug("hello") 60 | Logger.flush() 61 | end) =~ "hello" 62 | end 63 | 64 | test "configures format" do 65 | LoggerBackends.configure(LoggerBackends.Console, format: "$message [$level]") 66 | 67 | assert capture_log(fn -> Logger.debug("hello") end) =~ "hello [debug]" 68 | end 69 | 70 | test "configures metadata" do 71 | LoggerBackends.configure(LoggerBackends.Console, 72 | format: "$metadata$message", 73 | metadata: [:user_id] 74 | ) 75 | 76 | assert capture_log(fn -> Logger.debug("hello") end) =~ "hello" 77 | 78 | Logger.metadata(user_id: 11) 79 | Logger.metadata(user_id: 13) 80 | assert capture_log(fn -> Logger.debug("hello") end) =~ "user_id=13 hello" 81 | end 82 | 83 | test "logs initial_call as metadata" do 84 | LoggerBackends.configure(LoggerBackends.Console, 85 | format: "$metadata$message", 86 | metadata: [:initial_call] 87 | ) 88 | 89 | assert capture_log(fn -> Logger.debug("hello", initial_call: {Foo, :bar, 3}) end) =~ 90 | "initial_call=Foo.bar/3 hello" 91 | end 92 | 93 | test "logs domain as metadata" do 94 | LoggerBackends.configure(LoggerBackends.Console, 95 | format: "$metadata$message", 96 | metadata: [:domain] 97 | ) 98 | 99 | assert capture_log(fn -> Logger.debug("hello", domain: [:foobar]) end) =~ 100 | "domain=elixir.foobar hello" 101 | end 102 | 103 | test "logs mfa as metadata" do 104 | LoggerBackends.configure(LoggerBackends.Console, 105 | format: "$metadata$message", 106 | metadata: [:mfa] 107 | ) 108 | 109 | {function, arity} = __ENV__.function 110 | mfa = Exception.format_mfa(__MODULE__, function, arity) 111 | 112 | assert capture_log(fn -> Logger.debug("hello") end) =~ 113 | "mfa=#{mfa} hello" 114 | end 115 | 116 | test "ignores crash_reason metadata when configured with metadata: :all" do 117 | LoggerBackends.configure(LoggerBackends.Console, format: "$metadata$message", metadata: :all) 118 | Logger.metadata(crash_reason: {%RuntimeError{message: "oops"}, []}) 119 | assert capture_log(fn -> Logger.debug("hello") end) =~ "hello" 120 | end 121 | 122 | test "configures formatter to {module, function} tuple" do 123 | LoggerBackends.configure(LoggerBackends.Console, format: {__MODULE__, :format}) 124 | 125 | assert capture_log(fn -> Logger.debug("hello") end) =~ "my_format: hello" 126 | end 127 | 128 | def format(_level, message, _ts, _metadata) do 129 | "my_format: #{message}" 130 | end 131 | 132 | test "configures metadata to :all" do 133 | LoggerBackends.configure(LoggerBackends.Console, format: "$metadata", metadata: :all) 134 | Logger.metadata(user_id: 11) 135 | Logger.metadata(dynamic_metadata: 5) 136 | 137 | %{module: mod, function: {name, arity}, file: file, line: line} = __ENV__ 138 | log = capture_log(fn -> Logger.debug("hello") end) 139 | 140 | assert log =~ "file=#{file}" 141 | assert log =~ "line=#{line + 1}" 142 | assert log =~ "module=#{inspect(mod)}" 143 | assert log =~ "function=#{name}/#{arity}" 144 | assert log =~ "dynamic_metadata=5" 145 | assert log =~ "user_id=11" 146 | end 147 | 148 | test "provides metadata defaults" do 149 | metadata = [:file, :line, :module, :function] 150 | LoggerBackends.configure(LoggerBackends.Console, format: "$metadata", metadata: metadata) 151 | 152 | %{module: mod, function: {name, arity}, file: file, line: line} = __ENV__ 153 | log = capture_log(fn -> Logger.debug("hello") end) 154 | 155 | assert log =~ "file=#{file} line=#{line + 1} module=#{inspect(mod)} function=#{name}/#{arity}" 156 | end 157 | 158 | test "configures level" do 159 | LoggerBackends.configure(LoggerBackends.Console, level: :info) 160 | 161 | assert capture_log(fn -> Logger.debug("hello") end) == "" 162 | end 163 | 164 | test "filter by notice" do 165 | LoggerBackends.configure(LoggerBackends.Console, level: :notice) 166 | 167 | assert capture_log(fn -> Logger.debug("hello") end) == "" 168 | assert capture_log(fn -> Logger.info("hello") end) == "" 169 | assert capture_log(fn -> Logger.notice("hello") end) =~ "[notice] hello\n" 170 | assert capture_log(fn -> Logger.warning("hello") end) =~ "[warning] hello\n" 171 | assert capture_log(fn -> Logger.critical("hello") end) =~ "[critical] hello\n" 172 | assert capture_log(fn -> Logger.alert("hello") end) =~ "[alert] hello\n" 173 | assert capture_log(fn -> Logger.error("hello") end) =~ "[error] hello\n" 174 | end 175 | 176 | test "configures colors" do 177 | LoggerBackends.configure(LoggerBackends.Console, format: "$message", colors: [enabled: true]) 178 | 179 | assert capture_log(fn -> Logger.debug("hello") end) == 180 | IO.ANSI.cyan() <> "hello" <> IO.ANSI.reset() 181 | 182 | LoggerBackends.configure(LoggerBackends.Console, colors: [debug: :magenta]) 183 | 184 | assert capture_log(fn -> Logger.debug("hello") end) == 185 | IO.ANSI.magenta() <> "hello" <> IO.ANSI.reset() 186 | 187 | assert capture_log(fn -> Logger.info("hello") end) == 188 | IO.ANSI.normal() <> "hello" <> IO.ANSI.reset() 189 | 190 | LoggerBackends.configure(LoggerBackends.Console, colors: [info: :cyan]) 191 | 192 | assert capture_log(fn -> Logger.info("hello") end) == 193 | IO.ANSI.cyan() <> "hello" <> IO.ANSI.reset() 194 | 195 | assert capture_log(fn -> Logger.warning("hello") end) == 196 | IO.ANSI.yellow() <> "hello" <> IO.ANSI.reset() 197 | 198 | LoggerBackends.configure(LoggerBackends.Console, colors: [warning: :magenta]) 199 | 200 | assert capture_log(fn -> Logger.warning("hello") end) == 201 | IO.ANSI.magenta() <> "hello" <> IO.ANSI.reset() 202 | 203 | assert capture_log(fn -> Logger.error("hello") end) == 204 | IO.ANSI.red() <> "hello" <> IO.ANSI.reset() 205 | 206 | LoggerBackends.configure(LoggerBackends.Console, colors: [error: :cyan]) 207 | 208 | assert capture_log(fn -> Logger.error("hello") end) == 209 | IO.ANSI.cyan() <> "hello" <> IO.ANSI.reset() 210 | end 211 | 212 | test "uses colors from metadata" do 213 | LoggerBackends.configure(LoggerBackends.Console, format: "$message", colors: [enabled: true]) 214 | 215 | assert capture_log(fn -> Logger.log(:error, "hello", ansi_color: :yellow) end) == 216 | IO.ANSI.yellow() <> "hello" <> IO.ANSI.reset() 217 | end 218 | end 219 | -------------------------------------------------------------------------------- /test/logger_backends/handler_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LoggerBackends.HandlerTest do 2 | use Logger.Case 3 | @moduletag :logger 4 | 5 | defmodule CustomTranslator do 6 | def t(:debug, _level, :format, {~c"hello: ~p", [:ok]}) do 7 | :skip 8 | end 9 | 10 | def t(:debug, _level, :format, {~c"world: ~p", [:ok]}) do 11 | {:ok, "rewritten"} 12 | end 13 | 14 | def t(:debug, :info, :report, {:logger, %{hello: :ok}}) do 15 | :skip 16 | end 17 | 18 | def t(:debug, :info, :report, {:logger, %{world: :ok}}) do 19 | {:ok, "rewritten"} 20 | end 21 | 22 | def t(:debug, :info, :report, {:logger, %{error: error}}) do 23 | raise(error) 24 | end 25 | 26 | def t(_, _, _, _) do 27 | :none 28 | end 29 | end 30 | 31 | setup_all do 32 | Application.put_env(:logger, :default_handler, false) 33 | Logger.App.stop() 34 | Application.start(:logger) 35 | LoggerBackends.add(LoggerBackends.Console) 36 | 37 | on_exit(fn -> 38 | LoggerBackends.remove(LoggerBackends.Console) 39 | Application.delete_env(:logger, :default_handler) 40 | Logger.App.stop() 41 | Application.start(:logger) 42 | end) 43 | end 44 | 45 | test "add_translator/1 and remove_translator/1 for error_logger" do 46 | assert Logger.add_translator({CustomTranslator, :t}) 47 | 48 | assert capture_log(fn -> 49 | :error_logger.info_msg(~c"hello: ~p", [:ok]) 50 | end) == "" 51 | 52 | assert capture_log(fn -> 53 | :error_logger.info_msg(~c"world: ~p", [:ok]) 54 | end) =~ "[notice] rewritten" 55 | after 56 | assert Logger.remove_translator({CustomTranslator, :t}) 57 | end 58 | 59 | test "add_translator/1 and remove_translator/1 for logger formats" do 60 | assert Logger.add_translator({CustomTranslator, :t}) 61 | 62 | assert capture_log(fn -> 63 | :logger.info(~c"hello: ~p", [:ok]) 64 | end) == "" 65 | 66 | assert capture_log(fn -> 67 | :logger.info(~c"world: ~p", [:ok]) 68 | end) =~ "[info] rewritten" 69 | 70 | assert capture_log(fn -> 71 | :logger.info(%{hello: :ok}) 72 | end) == "" 73 | 74 | assert capture_log(fn -> 75 | :logger.info(%{world: :ok}) 76 | end) =~ "[info] rewritten" 77 | after 78 | assert Logger.remove_translator({CustomTranslator, :t}) 79 | end 80 | 81 | test "handles translation error" do 82 | assert Logger.add_translator({CustomTranslator, :t}) 83 | 84 | message = capture_log(fn -> :logger.info(%{error: "oops"}) end) 85 | assert message =~ "[info] Failure while translating Erlang's logger event\n" 86 | assert message =~ "** (RuntimeError) oops\n" 87 | after 88 | assert Logger.remove_translator({CustomTranslator, :t}) 89 | end 90 | 91 | test "converts Erlang metadata" do 92 | LoggerBackends.configure(LoggerBackends.Console, 93 | metadata: [:file, :line, :module, :function] 94 | ) 95 | 96 | message = 97 | capture_log(fn -> 98 | :logger.info("ok", %{file: ~c"file.erl", line: 13, mfa: {Foo, :bar, 3}}) 99 | end) 100 | 101 | assert message =~ "module=Foo" 102 | assert message =~ "function=bar/3" 103 | assert message =~ "file=file.erl" 104 | assert message =~ "line=13" 105 | after 106 | LoggerBackends.configure(LoggerBackends.Console, metadata: []) 107 | end 108 | 109 | test "uses reporting callback with Elixir inspection" do 110 | assert capture_log(fn -> 111 | callback = fn %{hello: :world} -> {"~p~n", [:formatted]} end 112 | :logger.info(%{hello: :world}, %{report_cb: callback}) 113 | end) =~ "[info] :formatted" 114 | end 115 | 116 | test "uses Erlang log levels" do 117 | assert capture_log(fn -> :logger.emergency(~c"ok") end) =~ "[emergency] ok" 118 | assert capture_log(fn -> :logger.alert(~c"ok") end) =~ "[alert] ok" 119 | assert capture_log(fn -> :logger.critical(~c"ok") end) =~ "[critical] ok" 120 | assert capture_log(fn -> :logger.error(~c"ok") end) =~ "[error] ok" 121 | assert capture_log(fn -> :logger.warning(~c"ok") end) =~ "[warning] ok" 122 | assert capture_log(fn -> :logger.notice(~c"ok") end) =~ "[notice] ok" 123 | assert capture_log(fn -> :logger.info(~c"ok") end) =~ "[info] ok" 124 | assert capture_log(fn -> :logger.debug(~c"ok") end) =~ "[debug] ok" 125 | end 126 | 127 | test "include Erlang severity level information" do 128 | LoggerBackends.configure(LoggerBackends.Console, metadata: [:erl_level]) 129 | 130 | assert capture_log(fn -> :logger.emergency(~c"ok") end) =~ "erl_level=emergency" 131 | assert capture_log(fn -> :logger.alert(~c"ok") end) =~ "erl_level=alert" 132 | assert capture_log(fn -> :logger.critical(~c"ok") end) =~ "erl_level=critical" 133 | assert capture_log(fn -> :logger.error(~c"ok") end) =~ "erl_level=error" 134 | assert capture_log(fn -> :logger.warning(~c"ok") end) =~ "erl_level=warning" 135 | assert capture_log(fn -> :logger.info(~c"ok") end) =~ "erl_level=info" 136 | assert capture_log(fn -> :logger.debug(~c"ok") end) =~ "erl_level=debug" 137 | 138 | [:emergency, :alert, :critical, :error, :warning, :notice, :info, :debug] 139 | |> Enum.each(fn level -> 140 | assert capture_log(fn -> :logger.log(level, ~c"ok") end) =~ "erl_level=#{level}" 141 | end) 142 | after 143 | LoggerBackends.configure(LoggerBackends.Console, metadata: []) 144 | end 145 | 146 | test "respects translator_inspect_opts for reports" do 147 | Application.put_env(:logger, :translator_inspect_opts, printable_limit: 1) 148 | 149 | assert capture_log(fn -> :logger.error(%{foo: "bar"}) end) =~ 150 | ~S([error] [foo: "b" <> ...]) 151 | after 152 | Application.put_env(:logger, :translator_inspect_opts, []) 153 | end 154 | 155 | test "calls report_cb/1 when supplied" do 156 | report = %{foo: "bar"} 157 | 158 | assert capture_log(fn -> :logger.error(report, %{report_cb: &format_report/1}) end) =~ 159 | ~S([error] %{foo: "bar"}) 160 | 161 | assert_received {:format, ^report} 162 | end 163 | 164 | test "calls report_cb/2 when supplied" do 165 | report = %{foo: "bar"} 166 | 167 | assert capture_log(fn -> :logger.error(report, %{report_cb: &format_report/2}) end) =~ 168 | ~S([error] %{foo: "bar"}) 169 | 170 | assert_received {:format, ^report, opts} when is_map(opts) 171 | assert %{chars_limit: 8096, depth: :unlimited, single_line: false} = opts 172 | end 173 | 174 | test "calls report_cb/2 when passed %{label: term(), report: term()}" do 175 | report = %{label: :foo, report: %{bar: 1, baz: 2}} 176 | 177 | assert capture_log(fn -> :logger.error(report, %{report_cb: &format_report/1}) end) 178 | assert_received {:format, ^report} 179 | 180 | assert capture_log(fn -> :logger.error(report, %{report_cb: &format_report/2}) end) 181 | assert_received {:format, ^report, _opts} 182 | end 183 | 184 | defp format_report(report) do 185 | send(self(), {:format, report}) 186 | 187 | {~c"~p", [report]} 188 | end 189 | 190 | defp format_report(report, opts) do 191 | send(self(), {:format, report, opts}) 192 | 193 | inspect(report) 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /test/logger_backends_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LoggerBackendsTest do 2 | use Logger.Case 3 | require Logger 4 | 5 | defmodule MyBackend do 6 | @behaviour :gen_event 7 | 8 | def init({MyBackend, pid}) when is_pid(pid) do 9 | {:ok, pid} 10 | end 11 | 12 | def handle_event(event, state) do 13 | send(state, {:event, event}) 14 | {:ok, state} 15 | end 16 | 17 | def handle_call(:error, _) do 18 | raise "oops" 19 | end 20 | 21 | def handle_info(_msg, state) do 22 | {:ok, state} 23 | end 24 | 25 | def code_change(_old_vsn, state, _extra) do 26 | {:ok, state} 27 | end 28 | 29 | def terminate(_reason, _state) do 30 | :ok 31 | end 32 | end 33 | 34 | test "add_backend/1 and remove_backend/1" do 35 | assert {:ok, _pid} = LoggerBackends.add(LoggerBackends.Console) 36 | assert LoggerBackends.add(LoggerBackends.Console) == {:error, :already_present} 37 | assert :ok = LoggerBackends.remove(LoggerBackends.Console) 38 | assert LoggerBackends.remove(LoggerBackends.Console) == {:error, :not_found} 39 | end 40 | 41 | test "add_backend/1 with {module, id}" do 42 | assert {:ok, _} = LoggerBackends.add({MyBackend, self()}) 43 | assert {:error, :already_present} = LoggerBackends.add({MyBackend, self()}) 44 | assert :ok = LoggerBackends.remove({MyBackend, self()}) 45 | end 46 | 47 | test "add_backend/1 with unknown backend" do 48 | assert {:error, {{:EXIT, {:undef, [_ | _]}}, _}} = 49 | LoggerBackends.add({UnknownBackend, self()}) 50 | end 51 | 52 | test "logs or writes to stderr on failed call on async mode" do 53 | assert {:ok, _} = LoggerBackends.add({MyBackend, self()}) 54 | 55 | assert capture_log(fn -> 56 | ExUnit.CaptureIO.capture_io(:stderr, fn -> 57 | :gen_event.call(LoggerBackends, {MyBackend, self()}, :error) 58 | wait_for_handler(LoggerBackends, {MyBackend, self()}) 59 | end) 60 | end) =~ 61 | ~r":gen_event handler {LoggerBackendsTest.MyBackend, #PID<.*>} installed in LoggerBackends terminating" 62 | 63 | Logger.flush() 64 | after 65 | LoggerBackends.remove({MyBackend, self()}) 66 | end 67 | 68 | test "logs or writes to stderr on failed call on sync mode" do 69 | LoggerBackends.configure(sync_threshold: 0) 70 | assert {:ok, _} = LoggerBackends.add({MyBackend, self()}) 71 | 72 | assert capture_log(fn -> 73 | ExUnit.CaptureIO.capture_io(:stderr, fn -> 74 | :gen_event.call(LoggerBackends, {MyBackend, self()}, :error) 75 | wait_for_handler(LoggerBackends, {MyBackend, self()}) 76 | end) 77 | end) =~ 78 | ~r":gen_event handler {LoggerBackendsTest.MyBackend, #PID<.*>} installed in LoggerBackends terminating" 79 | 80 | Logger.flush() 81 | after 82 | LoggerBackends.configure(sync_threshold: 20) 83 | LoggerBackends.remove({MyBackend, :hello}) 84 | end 85 | 86 | test "logs when discarding messages" do 87 | assert :ok = LoggerBackends.configure(discard_threshold: 5) 88 | LoggerBackends.add({MyBackend, self()}) 89 | 90 | capture_log(fn -> 91 | :sys.suspend(LoggerBackends) 92 | for _ <- 1..10, do: Logger.warning("warning!") 93 | :sys.resume(LoggerBackends) 94 | Logger.flush() 95 | send(LoggerBackends, {LoggerBackends.Config, :update_counter}) 96 | end) 97 | 98 | assert_receive {:event, 99 | {:warning, _, 100 | {Logger, "Attempted to log 0 messages, which is below :discard_threshold", 101 | _time, _metadata}}} 102 | after 103 | :sys.resume(LoggerBackends) 104 | LoggerBackends.remove({MyBackend, self()}) 105 | assert :ok = LoggerBackends.configure(discard_threshold: 500) 106 | end 107 | 108 | test "restarts LoggerBackends.Config on Logger exits" do 109 | LoggerBackends.configure([]) 110 | 111 | capture_log(fn -> 112 | Process.whereis(LoggerBackends) |> Process.exit(:kill) 113 | wait_for_logger() 114 | wait_for_handler(LoggerBackends, LoggerBackends.Config) 115 | end) 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule Logger.Case do 4 | use ExUnit.CaseTemplate 5 | import ExUnit.CaptureIO 6 | 7 | using _ do 8 | quote do 9 | import Logger.Case 10 | end 11 | end 12 | 13 | def msg(msg) do 14 | ~r/\d\d\:\d\d\:\d\d\.\d\d\d #{Regex.escape(msg)}/ 15 | end 16 | 17 | def wait_for_handler(manager, handler) do 18 | unless handler in :gen_event.which_handlers(manager) do 19 | Process.sleep(10) 20 | wait_for_handler(manager, handler) 21 | end 22 | end 23 | 24 | def wait_for_logger() do 25 | try do 26 | :gen_event.which_handlers(LoggerBackends) 27 | catch 28 | :exit, _ -> 29 | Process.sleep(10) 30 | wait_for_logger() 31 | else 32 | _ -> 33 | :ok 34 | end 35 | end 36 | 37 | def capture_log(level \\ :debug, fun) do 38 | Logger.configure(level: level) 39 | 40 | capture_io(:user, fn -> 41 | fun.() 42 | Logger.flush() 43 | end) 44 | after 45 | Logger.configure(level: :debug) 46 | end 47 | end 48 | --------------------------------------------------------------------------------