├── .gitignore
├── CHANGELOG.md
├── README.md
├── config
└── config.exs
├── lib
├── logger.ex
└── logger
│ ├── backends
│ └── console.ex
│ ├── config.ex
│ ├── error_handler.ex
│ ├── formatter.ex
│ ├── translator.ex
│ ├── utils.ex
│ └── watcher.ex
├── mix.exs
└── test
├── logger
├── backends
│ └── console_test.exs
├── error_handler_test.exs
├── formatter_test.exs
├── translator_test.exs
└── utils_test.exs
├── logger_test.exs
└── test_helper.exs
/.gitignore:
--------------------------------------------------------------------------------
1 | /_build
2 | /deps
3 | erl_crash.dump
4 | *.ez
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # v0.4.0
2 |
3 | * [Logger] Support custom backends
4 | * [Logger] Support custom translators
5 |
6 | # v0.3.0
7 |
8 | * [Logger] Requires Elixir master
9 | * [Logger] Improve truncation algorithms to avoid overflow
10 | * [Logger] Alternate between sync and async modes
11 | * [Logger] Prune messages based on logger level
12 | * [Logger] Provides custom formatting and API for customizing backends
13 | * [Logger] Allow users to choose in between utc or local time logging (defaults to local time)
14 |
15 | # v0.2.0
16 |
17 | * [Logger] Add debug level
18 | * [Logger] Add data truncation
19 | * [Logger] Add lazily calculated messages with functions
20 | * [Logger] Add a discard threshold for the error logger
21 |
22 | # v0.1.0
23 |
24 | * [Logger] Logger provides API for emitting error/info/warning messages
25 | * [Logger] Logger formats Erlang' error/info/warning messages in Elixir terms
26 | * [Logger] Logger provides a watcher to ensure the error logger handler is always reinstalled
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Logger
2 | ======
3 |
4 | **Note: this project has been merged into Elixir and is available since v0.15.0**
5 |
6 | The goal of this project is to explore a Logger implementation that will be included in Elixir. One of the big influences for this project is [Lager](https://github.com/basho/lager) and [Andrew's talk on the matter](http://www.youtube.com/watch?v=8BNpOHFvg_Q).
7 |
8 | Obviously, one of the first questions may be: why not Lager? We need a project that knows how to log terms in Elixir syntax, in particular, using the `Inspect` protocol. That's why the focus of this project is on the error handler and not on the error logger itself.
9 |
10 | By default `Logger` will run on top of OTP's `error_logger` and we will include an API that mostly wraps the `error_logger` one.
11 |
12 | ## Installation
13 |
14 | Add `:logger` as a dependency to your `mix.exs` file:
15 |
16 | ```elixir
17 | defp deps do
18 | [{:logger, github: "josevalim/logger"}]
19 | end
20 | ```
21 |
22 | You should also update your application list to include `:logger`:
23 |
24 | ```elixir
25 | def application do
26 | [applications: [:logger]]
27 | end
28 | ```
29 |
30 | Logger is not published on Hex as we intend to merge it into Elixir before 1.0.
31 |
32 | ## Features
33 |
34 | Below we detail the features we plan to include in the short-term, long-term or when it does not apply.
35 |
36 | Short-term features (before 1.0):
37 |
38 | * *done* A `Logger` module to log warning, info and error messages.
39 | * *done* A backend that can print log messages using Elixir terms.
40 | * *done* A watcher to ensure the handler is registered even if it crashes.
41 | * *done* Data truncation so we never try to log a message of megabytes of size.
42 | * *done* A way to lazily calculate the log messages to avoid generating expensive log messages that won't be used.
43 | * *done* An error handler that supports a threshold (as seen in Lager) to limit the amount of messages we print per second (so we never bring the node down due to excessive messages, see [cascading-failures](https://github.com/ferd/cascading-failures)).
44 | * *done* Switching between sync and async modes.
45 | * *done* Custom formatting.
46 | * *done* Metadata (via options and process dictionary).
47 | * *done* Error translators, so we can translate GenServer and other OTP errors into something more palatable.
48 | * *done* Custom backends.
49 |
50 | Long-term features (after 1.0):
51 |
52 | * *done* SASL reports.
53 | * Logging to files and log rotation.
54 | * Tracing.
55 |
56 | ## LICENSE
57 |
58 | This project is under the same LICENSE as Elixir.
59 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies. The Mix.Config module provides functions
3 | # to aid in doing so.
4 | use Mix.Config
5 |
6 | # Note this file is loaded before any dependency and is restricted
7 | # to this project. If another project depends on this project, this
8 | # file won't be loaded nor affect the parent project.
9 |
10 | # Sample configuration:
11 | #
12 | # config :my_dep,
13 | # key: :value,
14 | # limit: 42
15 |
16 | # It is also possible to import configuration files, relative to this
17 | # directory. For example, you can emulate configuration per environment
18 | # by uncommenting the line below and defining dev.exs, test.exs and such.
19 | # Configuration from the imported file will override the ones defined
20 | # here (which is why it is important to import them last).
21 | #
22 | # import_config "#{Mix.env}.exs"
23 |
--------------------------------------------------------------------------------
/lib/logger.ex:
--------------------------------------------------------------------------------
1 | defmodule Logger do
2 | use Application
3 |
4 | @moduledoc ~S"""
5 | A logger for Elixir applications.
6 |
7 | It includes many features:
8 |
9 | * Provides debug, info, warn and error levels.
10 |
11 | * Supports multiple backends which are automatically
12 | supervised when plugged into Logger.
13 |
14 | * Formats and truncates messages on the client
15 | to avoid clogging logger backends.
16 |
17 | * Alternates between sync and async modes to keep
18 | it performant when required but also apply back-
19 | pressure when under stress.
20 |
21 | * Wraps OTP's error_logger to avoid it from
22 | overflowing.
23 |
24 | ## Levels
25 |
26 | The supported levels are:
27 |
28 | * `:debug` - for debug-related messages
29 | * `:info` - for information of any kind
30 | * `:warn` - for warnings
31 | * `:error` - for errors
32 |
33 | ## Configuration
34 |
35 | Logger supports a wide range of configuration.
36 |
37 | This configuration is split in three categories:
38 |
39 | * Application configuration - must be set before the logger
40 | application is started
41 |
42 | * Runtime configuration - can be set before the logger
43 | application is started but changed during runtime
44 |
45 | * Error logger configuration - configuration for the
46 | wrapper around OTP's error_logger
47 |
48 | ### Application configuration
49 |
50 | The following configuration must be set via config files
51 | before the logger application is started.
52 |
53 | * `:backends` - the backends to be used. Defaults to `[:console]`.
54 | See the "Backends" section for more information.
55 |
56 | * `:compile_time_purge_level` - purge all calls that have log level
57 | lower than the configured value at compilation time. This means the
58 | Logger call will be completely removed at compile time, occuring
59 | no overhead at runtime. By default, defaults to `:debug` and only
60 | applies to the `Logger.debug`, `Logger.info`, etc style of calls.
61 |
62 | ### Runtime Configuration
63 |
64 | All configuration below can be set via the config files but also
65 | changed dynamically during runtime via `Logger.configure/1`.
66 |
67 | * `:level` - the logging level. Attempting to log any message
68 | with severity less than the configured level will simply
69 | cause the message to be ignored. Keep in mind that each backend
70 | may have its specific level too.
71 |
72 | * `:utc_log` - when true, uses UTC in logs. By default it uses
73 | local time (i.e. it defaults to false).
74 |
75 | * `:truncate` - the maximum message size to be logged. Defaults
76 | to 8192 bytes. Note this configuration is approximate. Truncated
77 | messages will have " (truncated)" at the end.
78 |
79 | * `:sync_threshold` - if the logger manager has more than
80 | `sync_threshold` messages in its queue, logger will change
81 | to sync mode, to apply back-pressure to the clients.
82 | Logger will return to sync mode once the number of messages
83 | in the queue reduce to `sync_threshold * 0.75` messages.
84 | Defaults to 20 messages.
85 |
86 | ### Error logger configuration
87 |
88 | The following configuration applies to the Logger wrapper around
89 | Erlang's error_logger. All the configurations below must be set
90 | before the logger application starts.
91 |
92 | * `:handle_otp_reports` - redirects OTP reports to Logger so
93 | they are formatted in Elixir terms. This uninstalls Erlang's
94 | logger that prints terms to terminal.
95 |
96 | * `:discard_threshold_for_error_logger` - a value that, when
97 | reached, triggers the error logger to discard messages. This
98 | value must be a positive number that represents the maximum
99 | number of messages accepted per second. Once above this
100 | threshold, the error_logger enters in discard mode for the
101 | remaining of that second. Defaults to 500 messages.
102 |
103 | Furthermore, Logger allows messages sent by Erlang's `error_logger`
104 | to be translated into an Elixir format via translators. Translator
105 | can be dynamically added at any time with the `add_translator/1`
106 | and `remove_translator/1` APIs. Check `Logger.Translator` for more
107 | information.
108 |
109 | ## Backends
110 |
111 | Logger supports different backends where log messages are written to.
112 |
113 | The available backends by default are:
114 |
115 | * `:console` - Logs messages to the console (enabled by default)
116 |
117 | Developers may also implement their own backends, an option that
118 | is explored with detail below.
119 |
120 | The initial backends are loaded via the `:backends` configuration,
121 | which must be set before the logger application is started. However,
122 | backends can be added or removed dynamically via the `add_backend/2`,
123 | `remove_backend/1` and `configure_backend/2` functions. Note though
124 | that dynamically added backends are not restarded in case of crashes.
125 |
126 | ### Console backend
127 |
128 | The console backend logs message to the console. It supports the
129 | following options:
130 |
131 | * `:level` - the level to be logged by this backend.
132 | Note though messages are first filtered by the general
133 | `:level` configuration in `:logger`
134 |
135 | * `:format` - the format message used to print logs.
136 | Defaults to: "$time $metadata[$level] $message\n"
137 |
138 | * `:metadata` - the metadata to be printed by `$metadata`.
139 | Defaults to an empty list (no metadata)
140 |
141 | Here is an example on how to configure the `:console` in a
142 | `config/config.exs` file:
143 |
144 | config :logger, :console,
145 | format: "$date $time [$level] $metadata$message\n",
146 | metadata: [:user_id]
147 |
148 | You can read more about formatting in `Logger.Formatter`.
149 |
150 | ### Custom backends
151 |
152 | Any developer can create their own backend for Logger.
153 | Since Logger is an event manager powered by `GenEvent`,
154 | writing a new backend is a matter of creating an event
155 | handler, as described in the `GenEvent` module.
156 |
157 | From now on, we will be using event handler to refer to
158 | your custom backend, as we head into implementation details.
159 |
160 | The `add_backend/1` function is used to start a new
161 | backend, which installs the given event handler to the
162 | Logger event manager. This event handler is automatically
163 | supervised by Logger.
164 |
165 | Once added, the handler should be able to handle events
166 | in the following format:
167 |
168 | {level, group_leader,
169 | {Logger, message, timestamp, metadata}}
170 |
171 | The level is one of `:error`, `:info`, `:warn` or `:error`,
172 | as previously described, the group leader is the group
173 | leader of the process who logged the message, followed by
174 | a tuple starting with the atom `Logger`, the message as
175 | iodata, the timestamp and a keyword list of metadata.
176 |
177 | It is recommended that handlers ignore messages where
178 | the group leader is in a different node than the one
179 | the handler is installed.
180 |
181 | Furthermore, backends can be configured via the `configure_backend/2`
182 | function which requires event handlers to handle calls of
183 | the following format:
184 |
185 | {:configure, options}
186 |
187 | where options is a keyword list. The result of the call is
188 | the result returned by `configure_backend/2`. You may simply
189 | return `:ok` if you don't perform any kind of validation.
190 |
191 | It is recommended that backends support at least the following
192 | configuration values:
193 |
194 | * level - the logging level for that backend
195 | * format - the logging format for that backend
196 | * metadata - the metadata to include the backend
197 |
198 | Check the implementation for `Logger.Backends.Console` for
199 | examples on how to handle the recommendations in this section
200 | and how to process the existing options.
201 | """
202 |
203 | @type backend :: GenEvent.handler
204 | @type level :: :error | :info | :warn | :debug
205 | @levels [:error, :info, :warn, :debug]
206 |
207 | @doc false
208 | def start(_type, _args) do
209 | import Supervisor.Spec
210 |
211 | otp_reports? = Application.get_env(:logger, :handle_otp_reports)
212 | threshold = Application.get_env(:logger, :discard_threshold_for_error_logger)
213 |
214 | handlers =
215 | for backend <- Application.get_env(:logger, :backends) do
216 | {Logger, translate_backend(backend), []}
217 | end
218 |
219 | options = [strategy: :rest_for_one, name: Logger.Supervisor]
220 | children = [worker(GenEvent, [[name: Logger]]),
221 | worker(Logger.Watcher, [Logger, Logger.Config, []],
222 | [id: Logger.Config, function: :watcher]),
223 | supervisor(Logger.Watcher, [handlers]),
224 | worker(Logger.Watcher,
225 | [:error_logger, Logger.ErrorHandler, {otp_reports?, threshold}],
226 | [id: Logger.ErrorHandler, function: :watcher])]
227 |
228 | case Supervisor.start_link(children, options) do
229 | {:ok, _} = ok ->
230 | deleted = delete_error_logger_handler(otp_reports?, :error_logger_tty_h, [])
231 | store_deleted_handlers(deleted)
232 | ok
233 | {:error, _} = error ->
234 | error
235 | end
236 | end
237 |
238 | @doc false
239 | def stop(_) do
240 | Application.get_env(:logger, :deleted_handlers)
241 | |> Enum.each(&:error_logger.add_report_handler/1)
242 |
243 | # We need to do this in another process as the Application
244 | # Controller is currently blocked shutting down this app.
245 | spawn_link(fn -> Logger.Config.clear_data end)
246 |
247 | :ok
248 | end
249 |
250 | defp store_deleted_handlers(list) do
251 | Application.put_env(:logger, :deleted_handlers, Enum.into(list, HashSet.new))
252 | end
253 |
254 | defp delete_error_logger_handler(should_delete?, handler, deleted) do
255 | if should_delete? and
256 | :error_logger.delete_report_handler(handler) != {:error, :module_not_found} do
257 | [handler|deleted]
258 | else
259 | deleted
260 | end
261 | end
262 |
263 | @metadata :logger_metadata
264 |
265 | @doc """
266 | Adds the given keyword list to the current process metadata.
267 | """
268 | def metadata(dict) do
269 | Process.put(@metadata, dict ++ metadata)
270 | end
271 |
272 | @doc """
273 | Reads the current process metadata.
274 | """
275 | def metadata() do
276 | Process.get(@metadata) || []
277 | end
278 |
279 | @doc """
280 | Retrieves the logger level.
281 |
282 | The logger level can be changed via `configure/1`.
283 | """
284 | @spec level() :: level
285 | def level() do
286 | check_logger!
287 | %{level: level} = Logger.Config.__data__
288 | level
289 | end
290 |
291 | @doc """
292 | Compare log levels.
293 |
294 | Receives to log levels and compares the `left`
295 | against `right` and returns `:lt`, `:eq` or `:gt`.
296 | """
297 | @spec compare_levels(level, level) :: :lt | :eq | :gt
298 | def compare_levels(level, level), do:
299 | :eq
300 | def compare_levels(left, right), do:
301 | if(level_to_number(left) > level_to_number(right), do: :gt, else: :lt)
302 |
303 | defp level_to_number(:debug), do: 0
304 | defp level_to_number(:info), do: 1
305 | defp level_to_number(:warn), do: 2
306 | defp level_to_number(:error), do: 3
307 |
308 | @doc """
309 | Configures the logger.
310 |
311 | See the "Runtime Configuration" section in `Logger` module
312 | documentation for the available options.
313 | """
314 | @valid_options [:compile_time_purge_level, :sync_threshold, :truncate, :level, :utc_log]
315 |
316 | def configure(options) do
317 | Logger.Config.configure(Dict.take(options, @valid_options))
318 | end
319 |
320 | @doc """
321 | Adds a new backend.
322 | """
323 | def add_backend(backend) do
324 | Logger.Watcher.watch(Logger, translate_backend(backend), [])
325 | end
326 |
327 | @doc """
328 | Removes a backend.
329 | """
330 | def remove_backend(backend) do
331 | Logger.Watcher.unwatch(Logger, translate_backend(backend))
332 | end
333 |
334 | @doc """
335 | Adds a new translator.
336 | """
337 | def add_translator({mod, fun} = translator) when is_atom(mod) and is_atom(fun) do
338 | Logger.Config.add_translator(translator)
339 | end
340 |
341 | @doc """
342 | Removes a translator.
343 | """
344 | def remove_translator({mod, fun} = translator) when is_atom(mod) and is_atom(fun) do
345 | Logger.Config.remove_translator(translator)
346 | end
347 |
348 | @doc """
349 | Configures the given backend.
350 | """
351 | @spec configure_backend(backend, Keywowrd.t) :: term
352 | def configure_backend(backend, options) when is_list(options) do
353 | GenEvent.call(Logger, translate_backend(backend), {:configure, options})
354 | end
355 |
356 | defp translate_backend(:console), do: Logger.Backends.Console
357 | defp translate_backend(other), do: other
358 |
359 | @doc """
360 | Logs a message.
361 |
362 | Developers should rather use the macros `Logger.debug/2`,
363 | `Logger.warn/2`, `Logger.info/2` or `Logger.error/2` instead
364 | of this function as they automatically include caller metadata
365 | and can eliminate the Logger call altogether at compile time if
366 | desired.
367 |
368 | Use this function only when there is a need to log dynamically
369 | or you want to explicitly avoid embedding metadata.
370 | """
371 | @spec log(level, IO.chardata | (() -> IO.chardata), Keyword.t) :: :ok
372 | def log(level, chardata, metadata \\ []) when level in @levels and is_list(metadata) do
373 | check_logger!
374 | %{mode: mode, truncate: truncate,
375 | level: min_level, utc_log: utc_log?} = Logger.Config.__data__
376 |
377 | if compare_levels(level, min_level) != :lt do
378 | tuple = {Logger, truncate(chardata, truncate), Logger.Utils.timestamp(utc_log?),
379 | [pid: self()] ++ metadata() ++ metadata}
380 | notify(mode, {level, Process.group_leader(), tuple})
381 | end
382 |
383 | :ok
384 | end
385 |
386 | @doc """
387 | Logs a warning.
388 |
389 | ## Examples
390 |
391 | Logger.warn "knob turned too much to the right"
392 | Logger.warn fn -> "expensive to calculate warning" end
393 |
394 | """
395 | defmacro warn(chardata, metadata \\ []) do
396 | macro_log(:warn, chardata, metadata, __CALLER__)
397 | end
398 |
399 | @doc """
400 | Logs some info.
401 |
402 | ## Examples
403 |
404 | Logger.info "mission accomplished"
405 | Logger.info fn -> "expensive to calculate info" end
406 |
407 | """
408 | defmacro info(chardata, metadata \\ []) do
409 | macro_log(:info, chardata, metadata, __CALLER__)
410 | end
411 |
412 | @doc """
413 | Logs an error.
414 |
415 | ## Examples
416 |
417 | Logger.error "oops"
418 | Logger.error fn -> "expensive to calculate error" end
419 |
420 | """
421 | defmacro error(chardata, metadata \\ []) do
422 | macro_log(:error, chardata, metadata, __CALLER__)
423 | end
424 |
425 | @doc """
426 | Logs a debug message.
427 |
428 | ## Examples
429 |
430 | Logger.debug "hello?"
431 | Logger.debug fn -> "expensive to calculate debug" end
432 |
433 | """
434 | defmacro debug(chardata, metadata \\ []) do
435 | macro_log(:debug, chardata, metadata, __CALLER__)
436 | end
437 |
438 | defp macro_log(level, chardata, metadata, caller) do
439 | min_level = Application.get_env(:logger, :compile_time_purge_level, :debug)
440 | if compare_levels(level, min_level) != :lt do
441 | %{module: module, function: function, line: line} = caller
442 | caller = [module: module, function: function, line: line]
443 | quote do
444 | Logger.log(unquote(level), unquote(chardata), unquote(caller) ++ unquote(metadata))
445 | end
446 | else
447 | :ok
448 | end
449 | end
450 |
451 | defp truncate(data, n) when is_function(data, 0),
452 | do: Logger.Utils.truncate(data.(), n)
453 | defp truncate(data, n) when is_list(data) or is_binary(data),
454 | do: Logger.Utils.truncate(data, n)
455 |
456 | defp notify(:sync, msg), do: GenEvent.sync_notify(Logger, msg)
457 | defp notify(:async, msg), do: GenEvent.notify(Logger, msg)
458 |
459 | defp check_logger! do
460 | unless Process.whereis(Logger) do
461 | raise "Cannot log messages, the :logger application is not running"
462 | end
463 | end
464 | end
465 |
--------------------------------------------------------------------------------
/lib/logger/backends/console.ex:
--------------------------------------------------------------------------------
1 | defmodule Logger.Backends.Console do
2 | use GenEvent
3 |
4 | def init(_) do
5 | if user = Process.whereis(:user) do
6 | Process.group_leader(self(), user)
7 | {:ok, configure([])}
8 | else
9 | {:error, :ignore}
10 | end
11 | end
12 |
13 | def handle_call({:configure, options}, _state) do
14 | {:ok, :ok, configure(options)}
15 | end
16 |
17 | def handle_event({_level, gl, _event}, state) when node(gl) != node() do
18 | {:ok, state}
19 | end
20 |
21 | def handle_event({level, _gl, {Logger, msg, ts, md}}, %{level: min_level} = state) do
22 | if nil?(min_level) or Logger.compare_levels(level, min_level) != :lt do
23 | log_event(level, msg, ts, md, state)
24 | end
25 | {:ok, state}
26 | end
27 |
28 | ## Helpers
29 |
30 | defp configure(options) do
31 | console = Keyword.merge(Application.get_env(:logger, :console, []), options)
32 | Application.put_env(:logger, :console, console)
33 |
34 | format = console
35 | |> Keyword.get(:format)
36 | |> Logger.Formatter.compile
37 |
38 | level = Keyword.get(console, :level)
39 | metadata = Keyword.get(console, :metadata, [])
40 | %{format: format, metadata: metadata, level: level}
41 | end
42 |
43 | defp log_event(level, msg, ts, md, %{format: format, metadata: metadata}) do
44 | :io.put_chars :user,
45 | Logger.Formatter.format(format, level, msg, ts, Dict.take(md, metadata))
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/logger/config.ex:
--------------------------------------------------------------------------------
1 | defmodule Logger.Config do
2 | @moduledoc false
3 |
4 | use GenEvent
5 |
6 | @name __MODULE__
7 | @data :__data__
8 |
9 | def start_link do
10 | GenServer.start_link(__MODULE__, :ok, name: @name)
11 | end
12 |
13 | def configure(options) do
14 | GenEvent.call(Logger, @name, {:configure, options})
15 | end
16 |
17 | def add_translator(translator) do
18 | GenEvent.call(Logger, @name, {:add_translator, translator})
19 | end
20 |
21 | def remove_translator(translator) do
22 | GenEvent.call(Logger, @name, {:remove_translator, translator})
23 | end
24 |
25 | def __data__() do
26 | Application.get_env(:logger, @data)
27 | end
28 |
29 | def clear_data() do
30 | Application.delete_env(:logger, @data)
31 | end
32 |
33 | def restart do
34 | set = Application.get_env(:logger, :deleted_handlers)
35 | Application.put_env(:logger, :deleted_handlers, HashSet.new)
36 | Application.stop(:logger)
37 | Enum.each(set, &:error_logger.add_report_handler/1)
38 | Application.start(:logger)
39 | end
40 |
41 | ## Callbacks
42 |
43 | def init(_) do
44 | # Use previous data if available in case this handler crashed.
45 | state = __data__ || compute_state(:async)
46 | {:ok, state}
47 | end
48 |
49 | def handle_event({_type, gl, _msg} = event, state) when node(gl) != node() do
50 | # Cross node messages are always async which also
51 | # means this handler won't crash in case there is
52 | # no logger installed in the other node.
53 | GenEvent.notify({Logger, node(gl)}, event)
54 | {:ok, state}
55 | end
56 |
57 | def handle_event(_event, state) do
58 | {:message_queue_len, len} = Process.info(self(), :message_queue_len)
59 |
60 | cond do
61 | len > state.sync_threshold and state.mode == :async ->
62 | state = %{state | mode: :sync}
63 | persist(state)
64 | {:ok, state}
65 | len < state.async_threshold and state.mode == :sync ->
66 | state = %{state | mode: :async}
67 | persist(state)
68 | {:ok, state}
69 | true ->
70 | {:ok, state}
71 | end
72 | end
73 |
74 | def handle_call({:configure, options}, state) do
75 | Enum.each options, fn {key, value} ->
76 | Application.put_env(:logger, key, value)
77 | end
78 | {:ok, :ok, compute_state(state.mode)}
79 | end
80 |
81 | def handle_call({:add_translator, translator}, state) do
82 | state = update_translators(state, fn t -> [translator|List.delete(t, translator)] end)
83 | {:ok, :ok, state}
84 | end
85 |
86 | def handle_call({:remove_translator, translator}, state) do
87 | state = update_translators(state, &List.delete(&1, translator))
88 | {:ok, :ok, state}
89 | end
90 |
91 | ## Helpers
92 |
93 | defp update_translators(%{translators: translators} = state, fun) do
94 | translators = fun.(translators)
95 | Application.put_env(:logger, :translators, translators)
96 | persist %{state | translators: translators}
97 | end
98 |
99 | defp compute_state(mode) do
100 | level = Application.get_env(:logger, :level)
101 | utc_log = Application.get_env(:logger, :utc_log)
102 | truncate = Application.get_env(:logger, :truncate)
103 | translators = Application.get_env(:logger, :translators)
104 |
105 | sync_threshold = Application.get_env(:logger, :sync_threshold)
106 | async_threshold = trunc(sync_threshold * 0.75)
107 |
108 | persist %{level: level, mode: mode, truncate: truncate,
109 | utc_log: utc_log, sync_threshold: sync_threshold,
110 | async_threshold: async_threshold, translators: translators}
111 | end
112 |
113 | defp persist(state) do
114 | Application.put_env(:logger, @data, state)
115 | state
116 | end
117 | end
118 |
--------------------------------------------------------------------------------
/lib/logger/error_handler.ex:
--------------------------------------------------------------------------------
1 | defmodule Logger.ErrorHandler do
2 | use GenEvent
3 |
4 | require Logger
5 |
6 | def init({otp?, threshold}) do
7 | {:ok, %{otp: otp?, threshold: threshold,
8 | last_length: 0, last_time: :os.timestamp, dropped: 0}}
9 | end
10 |
11 | ## Handle event
12 |
13 | def handle_event({_type, gl, _msg}, state) when node(gl) != node() do
14 | {:ok, state}
15 | end
16 |
17 | def handle_event(event, state) do
18 | state = check_threshold(state)
19 | log_event(event, state)
20 | {:ok, state}
21 | end
22 |
23 | ## Helpers
24 |
25 | defp log_event({:error, _gl, {pid, format, data}}, %{otp: true}),
26 | do: log_event(:error, :format, pid, {format, data})
27 | defp log_event({:error_report, _gl, {pid, :std_error, format}}, %{otp: true}),
28 | do: log_event(:error, :report, pid, {:std_error, format})
29 |
30 | defp log_event({:warning_msg, _gl, {pid, format, data}}, %{otp: true}),
31 | do: log_event(:warn, :format, pid, {format, data})
32 | defp log_event({:warning_report, _gl, {pid, :std_warning, format}}, %{otp: true}),
33 | do: log_event(:warn, :report, pid, {:std_warning, format})
34 |
35 | defp log_event({:info_msg, _gl, {pid, format, data}}, %{otp: true}),
36 | do: log_event(:info, :format, pid, {format, data})
37 | defp log_event({:info_report, _gl, {pid, :std_info, format}}, %{otp: true}),
38 | do: log_event(:info, :report, pid, {:std_info, format})
39 |
40 | defp log_event(_, _state),
41 | do: :ok
42 |
43 | defp log_event(level, kind, pid, data) do
44 | %{level: min_level, truncate: truncate,
45 | utc_log: utc_log?, translators: translators} = Logger.Config.__data__
46 |
47 | if Logger.compare_levels(level, min_level) != :lt &&
48 | (message = translate(translators, min_level, level, kind, data, truncate)) do
49 | message = Logger.Utils.truncate(message, truncate)
50 |
51 | # Mode is always async to avoid clogging the error_logger
52 | GenEvent.notify(Logger,
53 | {level, Process.group_leader(),
54 | {Logger, message, Logger.Utils.timestamp(utc_log?), [pid: ensure_pid(pid)]}})
55 | end
56 |
57 | :ok
58 | end
59 |
60 | defp ensure_pid(pid) when is_pid(pid), do: pid
61 | defp ensure_pid(_), do: self()
62 |
63 | defp check_threshold(%{last_time: last_time, last_length: last_length,
64 | dropped: dropped, threshold: threshold} = state) do
65 | {m, s, _} = current_time = :os.timestamp
66 | current_length = message_queue_length()
67 |
68 | cond do
69 | match?({^m, ^s, _}, last_time) and current_length - last_length > threshold ->
70 | count = drop_messages(current_time, 0)
71 | %{state | dropped: dropped + count, last_length: message_queue_length()}
72 | match?({^m, ^s, _}, last_time) ->
73 | state
74 | true ->
75 | if dropped > 0 do
76 | Logger.warn "Logger dropped #{dropped} OTP/SASL messages as it " <>
77 | "exceeded the amount of #{threshold} messages/second"
78 | end
79 | %{state | dropped: 0, last_time: current_time, last_length: current_length}
80 | end
81 | end
82 |
83 | defp message_queue_length() do
84 | {:message_queue_len, len} = Process.info(self(), :message_queue_len)
85 | len
86 | end
87 |
88 | defp drop_messages({m, s, _} = last_time, count) do
89 | case :os.timestamp do
90 | {^m, ^s, _} ->
91 | receive do
92 | {:notify, _event} -> drop_messages(last_time, count + 1)
93 | after
94 | 0 -> count
95 | end
96 | _ ->
97 | count
98 | end
99 | end
100 |
101 | defp translate([{mod, fun}|t], min_level, level, kind, data, truncate) do
102 | case apply(mod, fun, [min_level, level, kind, data]) do
103 | {:ok, iodata} -> iodata
104 | :skip -> nil
105 | :none -> translate(t, min_level, level, kind, data, truncate)
106 | end
107 | end
108 |
109 | defp translate([], _min_level, _level, :format, {format, args}, truncate) do
110 | {format, args} = Logger.Utils.inspect(format, args, truncate)
111 | :io_lib.format(format, args)
112 | end
113 |
114 | defp translate([], _min_level, _level, :report, {_type, data}, _truncate) do
115 | Kernel.inspect(data)
116 | end
117 | end
118 |
--------------------------------------------------------------------------------
/lib/logger/formatter.ex:
--------------------------------------------------------------------------------
1 | import Kernel, except: [inspect: 2]
2 |
3 | defmodule Logger.Formatter do
4 | @moduledoc ~S"""
5 | Conveniences for formatting data for logs.
6 |
7 | This module allows developers to specify a string that
8 | serves as template for log messages, for example:
9 |
10 | $time $metadata[$level] $message\n
11 |
12 | Will print error messages as:
13 |
14 | 18:43:12.439 user_id=13 [error] Hello\n
15 |
16 | The valid parameters you can use are:
17 |
18 | * `$time` - time the log message was sent
19 | * `$date` - date the log message was sent
20 | * `$message` - the log message
21 | * `$level` - the log level
22 | * `$node` - the node that prints the message
23 | * `$metadata` - user controled data presented in "key=val key2=val2" format
24 |
25 | Backends typically allow developers to supply such control
26 | strings via configuration files. This module provides `compile/1`,
27 | which compiles the string into a format for fast operations at
28 | runtime and `format/5` to format the compiled pattern into an
29 | actual IO data.
30 |
31 | ## Metadata
32 |
33 | Metadata to be sent to the Logger can be read and written with
34 | the `Logger.metadata/0` and `Logger.metadata/1` functions. For example,
35 | you can set `Logger.metadata([user_id: 13])` to add user_id metadata
36 | to the current process. The user can configure the backend to chose
37 | which metadata it wants to print and it will replace the $metadata
38 | value.
39 | """
40 |
41 | @valid_patterns [:time, :date, :message, :level, :node, :metadata]
42 | @default_pattern "$time $metadata[$level] $message\n"
43 |
44 | @doc ~S"""
45 | Compiles a format string into an array that the `format/5` can handle.
46 |
47 | The valid parameters you can use are:
48 |
49 | * $time
50 | * $date
51 | * $message
52 | * $level
53 | * $node
54 | * $metadata - metadata is presented in key=val key2=val2 format.
55 |
56 | If you pass nil into compile it will use the default
57 | format of `$time $metadata [$level] $message`
58 |
59 | If you would like to make your own custom formatter simply pass
60 | `{module, function}` to compile and the rest is handled.
61 |
62 | iex> Logger.Formatter.compile("$time $metadata [$level] $message\n")
63 | [:time, " ", :metadata, " [", :level, "] ", :message, "\n"]
64 | """
65 | @spec compile(binary | nil) :: list()
66 | @spec compile({atom, atom}) :: {atom, atom}
67 |
68 | def compile(nil), do: compile(@default_pattern)
69 | def compile({mod, fun}) when is_atom(mod) and is_atom(fun), do: {mod, fun}
70 |
71 | def compile(str) do
72 | for part <- Regex.split(~r/(?
)\$[a-z]+(?)/, str, on: [:head, :tail], trim: true) do
73 | case part do
74 | "$" <> code -> compile_code(String.to_atom(code))
75 | _ -> part
76 | end
77 | end
78 | end
79 |
80 | defp compile_code(key) when key in @valid_patterns, do: key
81 | defp compile_code(key) when is_atom(key) do
82 | raise(ArgumentError, message: "$#{key} is an invalid format pattern.")
83 | end
84 |
85 | @doc """
86 | Takes a compiled format and injects the, level, timestamp, message and
87 | metadata listdict and returns a properly formatted string.
88 | """
89 |
90 | def format({mod, fun}, level, msg, ts, md) do
91 | Module.function(mod, fun, 4).(level, msg, ts, md)
92 | end
93 |
94 | def format(config, level, msg, ts, md) do
95 | for c <- config do
96 | output(c, level, msg, ts, md)
97 | end
98 | end
99 |
100 | defp output(:message, _, msg, _, _), do: msg
101 | defp output(:date, _, _, {date, _time}, _), do: Logger.Utils.format_date(date)
102 | defp output(:time, _, _, {_date, time}, _), do: Logger.Utils.format_time(time)
103 | defp output(:level, level, _, _, _), do: Atom.to_string(level)
104 | defp output(:node, _, _, _, _), do: Atom.to_string(node())
105 | defp output(:metadata, _, _, _, []), do: ""
106 | defp output(:metadata, _, _, _, meta) do
107 | Enum.map(meta, fn {key, val} ->
108 | [to_string(key), ?=, to_string(val), ?\s]
109 | end)
110 | end
111 | defp output(other, _, _, _, _), do: other
112 | end
113 |
--------------------------------------------------------------------------------
/lib/logger/translator.ex:
--------------------------------------------------------------------------------
1 | defmodule Logger.Translator do
2 | @moduledoc """
3 | Default translation for Erlang log messages.
4 |
5 | Logger allows developers to rewrite log messages provided by
6 | Erlang applications into a format more compatible to Elixir
7 | log messages by providing translator.
8 |
9 | A translator is simply a tuple containing a module and a function
10 | that can be added and removed via the `add_translator/1` and
11 | `remove_translator/1` functions and is invoked for every Erlang
12 | message above the minimum log level with four arguments:
13 |
14 | * `min_level` - the current Logger level
15 | * `level` - the level of the message being translator
16 | * `kind` - if the message is a report or a format
17 | * `data` - the data to format. If it is a report, it is a tuple
18 | with `{report_type, report_data}`, if it is a format, it is a
19 | tuple with `{format_message, format_args}`
20 |
21 | The function must return:
22 |
23 | * `{:ok, iodata}` - if the message was translated with its translation
24 | * `:skip` - if the message is not meant to be translated nor logged
25 | * `:none` - if there is no translation, which triggers the next translator
26 |
27 | See the function `translate/4` in this module for an example implementation
28 | and the default messages translated by Logger.
29 | """
30 |
31 | def translate(min_level, :error, :format, message) do
32 | case message do
33 | {'** Generic server ' ++ _, [name, last, state, reason]} ->
34 | msg = "GenServer #{inspect name} terminating\n"
35 | if min_level == :debug do
36 | msg = msg <> "Last message: #{inspect last}\n"
37 | <> "State: #{inspect state}\n"
38 | end
39 | {:ok, msg <> "** (exit) " <> Exception.format_exit(reason)}
40 |
41 | {'** gen_event handler ' ++ _, [name, manager, last, state, reason]} ->
42 | msg = "GenEvent handler #{inspect name} installed in #{inspect manager} terminating\n"
43 | if min_level == :debug do
44 | msg = msg <> "Last message: #{inspect last}\n"
45 | <> "State: #{inspect state}\n"
46 | end
47 | {:ok, msg <> "** (exit) " <> Exception.format_exit(reason)}
48 |
49 | {'** Task ' ++ _, [name, starter, function, args, reason]} ->
50 | msg = "Task #{inspect name} started from #{inspect starter} terminating\n" <>
51 | "Function: #{inspect function}\n" <>
52 | " Args: #{inspect args}\n" <>
53 | "** (exit) " <> Exception.format_exit(reason)
54 | {:ok, msg}
55 |
56 | _ ->
57 | :none
58 | end
59 | end
60 |
61 | def translate(_min_level, :info, :report,
62 | {:std_info, [application: app, exited: reason, type: _type]}) do
63 | {:ok, "Application #{app} exited with reason #{Exception.format_exit(reason)}"}
64 | end
65 |
66 | def translate(_min_level, _level, _kind, _message) do
67 | :none
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/lib/logger/utils.ex:
--------------------------------------------------------------------------------
1 | defmodule Logger.Utils do
2 | @moduledoc false
3 |
4 | @doc """
5 | Truncates a char data into n bytes.
6 |
7 | There is a chance we truncate in the middle of a grapheme
8 | cluster but we never truncate in the middle of a binary
9 | codepoint. For this reason, truncation is not exact.
10 | """
11 | @spec truncate(IO.chardata, non_neg_integer) :: IO.chardata
12 | def truncate(chardata, n) when n >= 0 do
13 | {chardata, n} = truncate_n(chardata, n)
14 | if n >= 0, do: chardata, else: [chardata, " (truncated)"]
15 | end
16 |
17 | defp truncate_n(_, n) when n < 0 do
18 | {"", n}
19 | end
20 |
21 | defp truncate_n(binary, n) when is_binary(binary) do
22 | remaining = n - byte_size(binary)
23 | if remaining < 0 do
24 | # There is a chance we are cutting at the wrong
25 | # place so we need to fix the binary.
26 | {fix_binary(binary_part(binary, 0, n)), remaining}
27 | else
28 | {binary, remaining}
29 | end
30 | end
31 |
32 | defp truncate_n(int, n) when int in 0..127, do: {int, n-1}
33 | defp truncate_n(int, n) when int in 127..0x07FF, do: {int, n-2}
34 | defp truncate_n(int, n) when int in 0x800..0xFFFF, do: {int, n-3}
35 | defp truncate_n(int, n) when int >= 0x10000 and is_integer(int), do: {int, n-4}
36 |
37 | defp truncate_n(list, n) when is_list(list) do
38 | truncate_n_list(list, n, [])
39 | end
40 |
41 | defp truncate_n_list(_, n, acc) when n < 0 do
42 | {:lists.reverse(acc), n}
43 | end
44 |
45 | defp truncate_n_list([h|t], n, acc) do
46 | {h, n} = truncate_n(h, n)
47 | truncate_n_list(t, n, [h|acc])
48 | end
49 |
50 | defp truncate_n_list([], n, acc) do
51 | {:lists.reverse(acc), n}
52 | end
53 |
54 | defp truncate_n_list(t, n, acc) do
55 | {t, n} = truncate_n(t, n)
56 | {:lists.reverse(acc, t), n}
57 | end
58 |
59 | defp fix_binary(binary) do
60 | # Use a thirteen-bytes offset to look back in the binary.
61 | # This should allow at least two codepoints of 6 bytes.
62 | suffix_size = min(byte_size(binary), 13)
63 | prefix_size = byte_size(binary) - suffix_size
64 | <> = binary
65 | prefix <> fix_binary(suffix, "")
66 | end
67 |
68 | defp fix_binary(<>, acc) do
69 | acc <> <> <> fix_binary(t, "")
70 | end
71 |
72 | defp fix_binary(<>, acc) do
73 | fix_binary(t, <>)
74 | end
75 |
76 | defp fix_binary(<<>>, _acc) do
77 | <<>>
78 | end
79 |
80 | @doc """
81 | Receives a format string and arguments and replace `~p`,
82 | `~P`, `~w` and `~W` by its inspected variants.
83 | """
84 | def inspect(format, args, truncate, opts \\ %Inspect.Opts{})
85 |
86 | def inspect(format, args, truncate, opts) when is_atom(format) do
87 | do_inspect(Atom.to_char_list(format), args, truncate, opts)
88 | end
89 |
90 | def inspect(format, args, truncate, opts) when is_binary(format) do
91 | do_inspect(:binary.bin_to_list(format), args, truncate, opts)
92 | end
93 |
94 | def inspect(format, args, truncate, opts) when is_list(format) do
95 | do_inspect(format, args, truncate, opts)
96 | end
97 |
98 | defp do_inspect(format, [], _truncate, _opts), do: {format, []}
99 | defp do_inspect(format, args, truncate, opts) do
100 | # A pre-pass that removes binaries from
101 | # arguments according to the truncate limit.
102 | {args, _} = Enum.map_reduce(args, truncate, fn arg, acc ->
103 | if is_binary(arg) do
104 | truncate_n(arg, acc)
105 | else
106 | {arg, acc}
107 | end
108 | end)
109 | do_inspect(format, args, [], [], opts)
110 | end
111 |
112 | defp do_inspect([?~|t], args, used_format, used_args, opts) do
113 | {t, args, cc_format, cc_args} = collect_cc(:width, t, args, [?~], [], opts)
114 | do_inspect(t, args, cc_format ++ used_format, cc_args ++ used_args, opts)
115 | end
116 |
117 | defp do_inspect([h|t], args, used_format, used_args, opts),
118 | do: do_inspect(t, args, [h|used_format], used_args, opts)
119 |
120 | defp do_inspect([], [], used_format, used_args, _opts),
121 | do: {:lists.reverse(used_format), :lists.reverse(used_args)}
122 |
123 | ## width
124 |
125 | defp collect_cc(:width, [?-|t], args, used_format, used_args, opts),
126 | do: collect_value(:width, t, args, [?-|used_format], used_args, opts, :precision)
127 |
128 | defp collect_cc(:width, t, args, used_format, used_args, opts),
129 | do: collect_value(:width, t, args, used_format, used_args, opts, :precision)
130 |
131 | ## precision
132 |
133 | defp collect_cc(:precision, [?.|t], args, used_format, used_args, opts),
134 | do: collect_value(:precision, t, args, [?.|used_format], used_args, opts, :pad_char)
135 |
136 | defp collect_cc(:precision, t, args, used_format, used_args, opts),
137 | do: collect_cc(:pad_char, t, args, used_format, used_args, opts)
138 |
139 | ## pad char
140 |
141 | defp collect_cc(:pad_char, [?.,?*|t], [arg|args], used_format, used_args, opts),
142 | do: collect_cc(:encoding, t, args, [?*,?.|used_format], [arg|used_args], opts)
143 |
144 | defp collect_cc(:pad_char, [?.,p|t], args, used_format, used_args, opts),
145 | do: collect_cc(:encoding, t, args, [p,?.|used_format], used_args, opts)
146 |
147 | defp collect_cc(:pad_char, t, args, used_format, used_args, opts),
148 | do: collect_cc(:encoding, t, args, used_format, used_args, opts)
149 |
150 | ## encoding
151 |
152 | defp collect_cc(:encoding, [?l|t], args, used_format, used_args, opts),
153 | do: collect_cc(:done, t, args, [?l|used_format], used_args, %{opts | char_lists: false})
154 |
155 | defp collect_cc(:encoding, [?t|t], args, used_format, used_args, opts),
156 | do: collect_cc(:done, t, args, [?t|used_format], used_args, opts)
157 |
158 | defp collect_cc(:encoding, t, args, used_format, used_args, opts),
159 | do: collect_cc(:done, t, args, used_format, used_args, opts)
160 |
161 | ## done
162 |
163 | defp collect_cc(:done, [?W|t], [data, limit|args], _used_format, _used_args, opts),
164 | do: collect_inspect(t, args, data, %{opts | limit: limit, width: :infinity})
165 |
166 | defp collect_cc(:done, [?w|t], [data|args], _used_format, _used_args, opts),
167 | do: collect_inspect(t, args, data, %{opts | width: :infinity})
168 |
169 | defp collect_cc(:done, [?P|t], [data, limit|args], _used_format, _used_args, opts),
170 | do: collect_inspect(t, args, data, %{opts | limit: limit})
171 |
172 | defp collect_cc(:done, [?p|t], [data|args], _used_format, _used_args, opts),
173 | do: collect_inspect(t, args, data, opts)
174 |
175 | defp collect_cc(:done, [h|t], args, used_format, used_args, _opts) do
176 | {args, used_args} = collect_cc(h, args, used_args)
177 | {t, args, [h|used_format], used_args}
178 | end
179 |
180 | defp collect_cc(?x, [a,prefix|args], used), do: {args, [prefix, a|used]}
181 | defp collect_cc(?X, [a,prefix|args], used), do: {args, [prefix, a|used]}
182 | defp collect_cc(?s, [a|args], used), do: {args, [a|used]}
183 | defp collect_cc(?e, [a|args], used), do: {args, [a|used]}
184 | defp collect_cc(?f, [a|args], used), do: {args, [a|used]}
185 | defp collect_cc(?g, [a|args], used), do: {args, [a|used]}
186 | defp collect_cc(?b, [a|args], used), do: {args, [a|used]}
187 | defp collect_cc(?B, [a|args], used), do: {args, [a|used]}
188 | defp collect_cc(?+, [a|args], used), do: {args, [a|used]}
189 | defp collect_cc(?#, [a|args], used), do: {args, [a|used]}
190 | defp collect_cc(?c, [a|args], used), do: {args, [a|used]}
191 | defp collect_cc(?i, [a|args], used), do: {args, [a|used]}
192 | defp collect_cc(?~, args, used), do: {args, used}
193 | defp collect_cc(?n, args, used), do: {args, used}
194 |
195 | defp collect_inspect(t, args, data, opts) do
196 | data =
197 | data
198 | |> Inspect.Algebra.to_doc(opts)
199 | |> Inspect.Algebra.format(opts.width)
200 | {t, args, 'st~', [data]}
201 | end
202 |
203 | defp collect_value(current, [?*|t], [arg|args], used_format, used_args, opts, next)
204 | when is_integer(arg) do
205 | collect_cc(next, t, args, [?*|used_format], [arg|used_args],
206 | put_value(opts, current, arg))
207 | end
208 |
209 | defp collect_value(current, [c|t], args, used_format, used_args, opts, next)
210 | when is_integer(c) and c >= ?0 and c <= ?9 do
211 | {t, c} = collect_value([c|t], [])
212 | collect_cc(next, t, args, c ++ used_format, used_args,
213 | put_value(opts, current, c |> :lists.reverse |> List.to_integer))
214 | end
215 |
216 | defp collect_value(_current, t, args, used_format, used_args, opts, next),
217 | do: collect_cc(next, t, args, used_format, used_args, opts)
218 |
219 | defp collect_value([c|t], buffer)
220 | when is_integer(c) and c >= ?0 and c <= ?9,
221 | do: collect_value(t, [c|buffer])
222 |
223 | defp collect_value(other, buffer),
224 | do: {other, buffer}
225 |
226 | defp put_value(opts, key, value) do
227 | if Map.has_key?(opts, key) do
228 | Map.put(opts, key, value)
229 | else
230 | opts
231 | end
232 | end
233 |
234 | @doc """
235 | Returns a timestamp that includes miliseconds.
236 | """
237 | def timestamp(utc_log?) do
238 | {_, _, micro} = now = :os.timestamp()
239 | {date, {hours, minutes, seconds}} =
240 | case utc_log? do
241 | true -> :calendar.now_to_universal_time(now)
242 | false -> :calendar.now_to_local_time(now)
243 | end
244 | {date, {hours, minutes, seconds, div(micro, 1000)}}
245 | end
246 |
247 | @doc """
248 | Formats time to an iodata.
249 | """
250 | def format_time({hh, mi, ss, ms}) do
251 | [pad2(hh), ?:, pad2(mi), ?:, pad2(ss), ?., pad3(ms)]
252 | end
253 |
254 | @doc """
255 | Formats date to an iodata.
256 | """
257 | def format_date({yy, mm, dd}) do
258 | [Integer.to_string(yy), ?-, pad2(mm), ?-, pad2(dd)]
259 | end
260 |
261 | defp pad3(int) when int < 100 and int > 10, do: [?0, Integer.to_string(int)]
262 | defp pad3(int) when int < 10, do: [?0, ?0, Integer.to_string(int)]
263 | defp pad3(int), do: Integer.to_string(int)
264 |
265 | defp pad2(int) when int < 10, do: [?0, Integer.to_string(int)]
266 | defp pad2(int), do: Integer.to_string(int)
267 | end
268 |
--------------------------------------------------------------------------------
/lib/logger/watcher.ex:
--------------------------------------------------------------------------------
1 | defmodule Logger.Watcher do
2 | @moduledoc false
3 |
4 | require Logger
5 | use GenServer
6 | @name Logger.Watcher
7 |
8 | @doc """
9 | Starts the watcher supervisor.
10 | """
11 | def start_link(handlers) do
12 | options = [strategy: :one_for_one, name: @name]
13 | case Supervisor.start_link([], options) do
14 | {:ok, _} = ok ->
15 | _ = for {mod, handler, args} <- handlers do
16 | {:ok, _} = watch(mod, handler, args)
17 | end
18 | ok
19 | {:error, _} = error ->
20 | error
21 | end
22 | end
23 |
24 | @doc """
25 | Removes the given handler.
26 | """
27 | def unwatch(mod, handler) do
28 | case Supervisor.terminate_child(@name, {mod, handler}) do
29 | :ok -> Supervisor.delete_child(@name, {mod, handler})
30 | res -> res
31 | end
32 | end
33 |
34 | @doc """
35 | Watches the given handler as part of the handler supervision tree.
36 | """
37 | def watch(mod, handler, args) do
38 | import Supervisor.Spec
39 | id = {mod, handler}
40 | child = worker(__MODULE__, [mod, handler, args],
41 | [id: id, function: :watcher, restart: :transient])
42 | case Supervisor.start_child(@name, child) do
43 | {:ok, _pid} = result ->
44 | result
45 | {:error, :already_present} ->
46 | _ = Supervisor.delete_child(@name, id)
47 | watch(mod, handler, args)
48 | {:error, _reason} = error ->
49 | error
50 | end
51 | end
52 |
53 | @doc """
54 | Starts a watcher server.
55 |
56 | This is useful when there is a need to start a handler
57 | outside of the handler supervision tree.
58 | """
59 | def watcher(mod, handler, args) do
60 | GenServer.start_link(__MODULE__, {mod, handler, args})
61 | end
62 |
63 | ## Callbacks
64 |
65 | def init({mod, handler, args}) do
66 | case :gen_event.add_sup_handler(mod, handler, args) do
67 | :ok -> {:ok, {mod, handler}}
68 | {:error, :ignore} -> :ignore
69 | {:error, reason} -> {:stop, reason}
70 | {:EXIT, reason} -> {:stop, reason}
71 | end
72 | end
73 |
74 | def handle_info({:gen_event_EXIT, handler, reason}, {_, handler} = state)
75 | when reason in [:normal, :shutdown] do
76 | {:stop, reason, state}
77 | end
78 |
79 | def handle_info({:gen_event_EXIT, handler, reason}, {mod, handler} = state) do
80 | Logger.error "GenEvent handler #{inspect handler} installed at #{inspect mod}\n" <>
81 | "** (exit) #{format_exit(reason)}"
82 | {:stop, reason, state}
83 | end
84 |
85 | def handle_info(_msg, state) do
86 | {:noreply, state}
87 | end
88 |
89 | defp format_exit({:EXIT, reason}), do: Exception.format_exit(reason)
90 | defp format_exit(other), do: inspect(other)
91 | end
92 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Logger.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :logger,
6 | version: "0.4.0",
7 | elixir: "~> 0.15.0-dev",
8 | deps: deps]
9 | end
10 |
11 | # Configuration for the OTP application
12 | #
13 | # Type `mix help compile.app` for more information
14 | def application do
15 | [applications: [],
16 | mod: {Logger, []},
17 | env: [level: :debug,
18 | utc_log: false,
19 | truncate: 8096,
20 | backends: [:console],
21 | translators: [{Logger.Translator, :translate}],
22 | sync_threshold: 20,
23 | handle_otp_reports: true,
24 | compile_time_purge_level: :debug,
25 | discard_threshold_for_error_logger: 500,
26 | console: []]]
27 | end
28 |
29 | # Dependencies can be hex.pm packages:
30 | #
31 | # {:mydep, "~> 0.3.0"}
32 | #
33 | # Or git/path repositories:
34 | #
35 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1"}
36 | #
37 | # Type `mix help deps` for more examples and options
38 | defp deps do
39 | []
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/logger/backends/console_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Logger.Backends.ConsoleTest do
2 | use Logger.Case
3 | require Logger
4 |
5 | setup do
6 | on_exit fn ->
7 | :ok = Logger.configure_backend(:console, [format: nil, level: nil, metadata: []])
8 | end
9 | end
10 |
11 | test "does not start when there is no user" do
12 | user = Process.whereis(:user)
13 |
14 | try do
15 | Process.unregister(:user)
16 | assert GenEvent.add_handler(Logger, Logger.Backends.Console, []) ==
17 | {:error, :ignore}
18 | after
19 | Process.register(user, :user)
20 | end
21 | end
22 |
23 | test "can configure format" do
24 | Logger.configure_backend(:console, format: "$message [$level]")
25 |
26 | assert capture_log(fn ->
27 | Logger.debug("hello")
28 | end) =~ "hello [debug]"
29 | end
30 |
31 | test "can configure metadata" do
32 | Logger.configure_backend(:console, format: "$metadata$message", metadata: [:user_id])
33 |
34 | assert capture_log(fn ->
35 | Logger.debug("hello")
36 | end) =~ "hello"
37 |
38 | Logger.metadata(user_id: 13)
39 |
40 | assert capture_log(fn ->
41 | Logger.debug("user_id=13 hello")
42 | end) =~ "hello"
43 | end
44 |
45 | test "can configure level" do
46 | Logger.configure_backend(:console, level: :info)
47 |
48 | assert capture_log(fn ->
49 | Logger.debug("hello")
50 | end) == ""
51 | end
52 | end
--------------------------------------------------------------------------------
/test/logger/error_handler_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Logger.ErrorHandlerTest do
2 | use Logger.Case
3 |
4 | test "survives after crashes" do
5 | assert error_log(:info_msg, "~p~n", []) == ""
6 | assert capture_log(fn ->
7 | wait_for_handler(:error_logger, Logger.ErrorHandler)
8 | end) =~ "[error] GenEvent handler Logger.ErrorHandler installed at :error_logger\n" <>
9 | "** (exit) an exception was raised:"
10 | assert error_log(:info_msg, "~p~n", [:hello]) =~ msg("[info] :hello\n")
11 | end
12 |
13 | test "survives after Logger exit" do
14 | Process.exit(Process.whereis(Logger), :kill)
15 | wait_for_logger()
16 | wait_for_handler(:error_logger, Logger.ErrorHandler)
17 | assert error_log(:info_msg, "~p~n", [:hello]) =~ msg("[info] :hello\n")
18 | end
19 |
20 | test "formats error_logger info message" do
21 | assert error_log(:info_msg, "hello", []) =~ msg("[info] hello")
22 | assert error_log(:info_msg, "~p~n", [:hello]) =~ msg("[info] :hello\n")
23 | end
24 |
25 | test "formats error_logger info report" do
26 | assert error_log(:info_report, "hello") =~ msg("[info] \"hello\"")
27 | assert error_log(:info_report, :hello) =~ msg("[info] :hello\n")
28 | assert error_log(:info_report, :special, :hello) == ""
29 | end
30 |
31 | test "formats error_logger error message" do
32 | assert error_log(:error_msg, "hello", []) =~ msg("[error] hello")
33 | assert error_log(:error_msg, "~p~n", [:hello]) =~ msg("[error] :hello\n")
34 | end
35 |
36 | test "formats error_logger error report" do
37 | assert error_log(:error_report, "hello") =~ msg("[error] \"hello\"")
38 | assert error_log(:error_report, :hello) =~ msg("[error] :hello\n")
39 | assert error_log(:error_report, :special, :hello) == ""
40 | end
41 |
42 | test "formats error_logger warning message" do
43 | # Warnings by default are logged as errors by Erlang
44 | assert error_log(:warning_msg, "hello", []) =~ msg("[error] hello")
45 | assert error_log(:warning_msg, "~p~n", [:hello]) =~ msg("[error] :hello\n")
46 | end
47 |
48 | test "formats error_logger warning report" do
49 | # Warnings by default are logged as errors by Erlang
50 | assert error_log(:warning_report, "hello") =~ msg("[error] \"hello\"")
51 | assert error_log(:warning_report, :hello) =~ msg("[error] :hello\n")
52 | assert error_log(:warning_report, :special, :hello) == ""
53 | end
54 |
55 | defp error_log(fun, format) do
56 | do_error_log(fun, [format])
57 | end
58 |
59 | defp error_log(fun, format, args) do
60 | do_error_log(fun, [format, args])
61 | end
62 |
63 | defp do_error_log(fun, args) do
64 | capture_log(fn -> apply(:error_logger, fun, args) end)
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/test/logger/formatter_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Logger.FormatterTest do
2 | use Logger.Case, async: true
3 | doctest Logger.Formatter
4 |
5 | import Logger.Formatter
6 |
7 | defmodule CompileMod do
8 | def format(_level, _msg, _ts, _md) do
9 | true
10 | end
11 | end
12 |
13 | test "compile/1 with nil" do
14 | assert compile(nil) ==
15 | [:time, " ", :metadata, "[", :level, "] ", :message, "\n"]
16 | end
17 |
18 | test "compile/1 with str" do
19 | assert compile("$level $time $date $metadata $message $node") ==
20 | Enum.intersperse([:level, :time, :date, :metadata, :message, :node], " ")
21 |
22 | assert_raise ArgumentError,"$bad is an invalid format pattern.", fn ->
23 | compile("$bad $good")
24 | end
25 | end
26 |
27 | test "compile/1 with {mod, fun}" do
28 | assert compile({CompileMod, :format}) == {CompileMod, :format}
29 | end
30 |
31 | test "format with {mod, fun}" do
32 | assert format({CompileMod, :format}, nil, nil, nil,nil) == true
33 | end
34 |
35 | test "format with format string" do
36 | compiled = compile("[$level] $message")
37 | assert format(compiled, :error, "hello", nil, []) ==
38 | ["[", "error", "] ", "hello"]
39 |
40 | compiled = compile("$node")
41 | assert format(compiled, :error, nil, nil, []) == [Atom.to_string(node())]
42 |
43 | compiled = compile("$metadata")
44 | assert IO.iodata_to_binary(format(compiled, :error, nil, nil, [meta: :data])) ==
45 | "meta=data "
46 | assert IO.iodata_to_binary(format(compiled, :error, nil, nil, [])) ==
47 | ""
48 |
49 | timestamp = {{2014, 12, 30}, {12, 6, 30, 100}}
50 | compiled = compile("$date $time")
51 | assert IO.iodata_to_binary(format(compiled, :error, nil, timestamp, [])) ==
52 | "2014-12-30 12:06:30.100"
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/test/logger/translator_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Logger.TranslatorTest do
2 | use Logger.Case
3 |
4 | defmodule MyGenServer do
5 | use GenServer
6 |
7 | def handle_call(:error, _, _) do
8 | raise "oops"
9 | end
10 | end
11 |
12 | defmodule MyGenEvent do
13 | use GenEvent
14 |
15 | def handle_call(:error, _) do
16 | raise "oops"
17 | end
18 | end
19 |
20 | test "translates GenServer crashes" do
21 | {:ok, pid} = GenServer.start(MyGenServer, :ok)
22 |
23 | assert capture_log(:info, fn ->
24 | catch_exit(GenServer.call(pid, :error))
25 | end) =~ """
26 | [error] GenServer #{inspect pid} terminating
27 | ** (exit) an exception was raised:
28 | ** (RuntimeError) oops
29 | """
30 | end
31 |
32 | test "translates GenServer crashes on debug" do
33 | {:ok, pid} = GenServer.start(MyGenServer, :ok)
34 |
35 | assert capture_log(:debug, fn ->
36 | catch_exit(GenServer.call(pid, :error))
37 | end) =~ """
38 | [error] GenServer #{inspect pid} terminating
39 | Last message: :error
40 | State: :ok
41 | ** (exit) an exception was raised:
42 | ** (RuntimeError) oops
43 | """
44 | end
45 |
46 | test "translates GenEvent crashes" do
47 | {:ok, pid} = GenEvent.start()
48 | :ok = GenEvent.add_handler(pid, MyGenEvent, :ok)
49 |
50 | assert capture_log(:info, fn ->
51 | GenEvent.call(pid, MyGenEvent, :error)
52 | end) =~ """
53 | [error] GenEvent handler Logger.TranslatorTest.MyGenEvent installed in #{inspect pid} terminating
54 | ** (exit) an exception was raised:
55 | ** (RuntimeError) oops
56 | """
57 | end
58 |
59 | test "translates GenEvent crashes on debug" do
60 | {:ok, pid} = GenEvent.start()
61 | :ok = GenEvent.add_handler(pid, MyGenEvent, :ok)
62 |
63 | assert capture_log(:debug, fn ->
64 | GenEvent.call(pid, MyGenEvent, :error)
65 | end) =~ """
66 | [error] GenEvent handler Logger.TranslatorTest.MyGenEvent installed in #{inspect pid} terminating
67 | Last message: :error
68 | State: :ok
69 | ** (exit) an exception was raised:
70 | ** (RuntimeError) oops
71 | """
72 | end
73 |
74 | test "translates Task crashes" do
75 | {:ok, pid} = Task.start_link(__MODULE__, :task, [self()])
76 |
77 | assert capture_log(fn ->
78 | ref = Process.monitor(pid)
79 | send(pid, :go)
80 | receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
81 | end) =~ """
82 | [error] Task #{inspect pid} started from #{inspect self} terminating
83 | Function: &Logger.TranslatorTest.task/1
84 | Args: [#{inspect self}]
85 | ** (exit) an exception was raised:
86 | ** (RuntimeError) oops
87 | """
88 | end
89 |
90 | test "translates application stop" do
91 | :ok = Application.start(:eex)
92 |
93 | assert capture_log(fn ->
94 | Application.stop(:eex)
95 | end) =~ msg("[info] Application eex exited with reason :stopped")
96 | end
97 |
98 | def task(parent) do
99 | Process.unlink(parent)
100 | receive do: (:go -> raise "oops")
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/test/logger/utils_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Logger.UtilsTest do
2 | use Logger.Case, async: true
3 |
4 | import Logger.Utils
5 |
6 | import Kernel, except: [inspect: 2]
7 | defp inspect(format, args), do: Logger.Utils.inspect(format, args, 10)
8 |
9 | test "truncate/2" do
10 | # ASCII binaries
11 | assert truncate("foo", 4) == "foo"
12 | assert truncate("foo", 3) == "foo"
13 | assert truncate("foo", 2) == ["fo", " (truncated)"]
14 |
15 | # UTF-8 binaries
16 | assert truncate("olá", 2) == ["ol", " (truncated)"]
17 | assert truncate("olá", 3) == ["ol", " (truncated)"]
18 | assert truncate("olá", 4) == "olá"
19 | assert truncate("ááááá:", 10) == ["ááááá", " (truncated)"]
20 | assert truncate("áááááá:", 10) == ["ááááá", " (truncated)"]
21 |
22 | # Charlists
23 | assert truncate('olá', 2) == ['olá', " (truncated)"]
24 | assert truncate('olá', 3) == ['olá', " (truncated)"]
25 | assert truncate('olá', 4) == 'olá'
26 |
27 | # Chardata
28 | assert truncate('ol' ++ "á", 2) == ['ol' ++ "", " (truncated)"]
29 | assert truncate('ol' ++ "á", 3) == ['ol' ++ "", " (truncated)"]
30 | assert truncate('ol' ++ "á", 4) == 'ol' ++ "á"
31 | end
32 |
33 | test "inspect/2 formats" do
34 | assert inspect('~p', [1]) == {'~ts', [["1"]]}
35 | assert inspect("~p", [1]) == {'~ts', [["1"]]}
36 | assert inspect(:"~p", [1]) == {'~ts', [["1"]]}
37 | end
38 |
39 | test "inspect/2 sigils" do
40 | assert inspect('~10.10tp', [1]) == {'~ts', [["1"]]}
41 | assert inspect('~-10.10tp', [1]) == {'~ts', [["1"]]}
42 |
43 | assert inspect('~10.10lp', [1]) == {'~ts', [["1"]]}
44 | assert inspect('~10.10x~p~n', [1, 2, 3]) == {'~10.10x~ts~n', [1, 2, ["3"]]}
45 | end
46 |
47 | test "inspect/2 with modifier t has no effect (as it is the default)" do
48 | assert inspect('~tp', [1]) == {'~ts', [["1"]]}
49 | assert inspect('~tw', [1]) == {'~ts', [["1"]]}
50 | end
51 |
52 | test "inspect/2 with modifier l always prints lists" do
53 | assert inspect('~lp', ['abc']) ==
54 | {'~ts', [["[", "97", ",", " ", "98", ",", " ", "99", "]"]]}
55 | assert inspect('~lw', ['abc']) ==
56 | {'~ts', [["[", "97", ",", " ", "98", ",", " ", "99", "]"]]}
57 | end
58 |
59 | test "inspect/2 with modifier for width" do
60 | assert inspect('~5lp', ['abc']) ==
61 | {'~ts', [["[", "97", ",", "\n ", "98", ",", "\n ", "99", "]"]]}
62 |
63 | assert inspect('~5lw', ['abc']) ==
64 | {'~ts', [["[", "97", ",", " ", "98", ",", " ", "99", "]"]]}
65 | end
66 |
67 | test "inspect/2 with modifier for limit" do
68 | assert inspect('~5lP', ['abc', 2]) ==
69 | {'~ts', [["[", "97", ",", "\n ", "98", ",", "\n ", "...", "]"]]}
70 |
71 | assert inspect('~5lW', ['abc', 2]) ==
72 | {'~ts', [["[", "97", ",", " ", "98", ",", " ", "...", "]"]]}
73 | end
74 |
75 | test "inspect/2 truncates binaries" do
76 | assert inspect('~ts', ["abcdeabcdeabcdeabcde"]) ==
77 | {'~ts', ["abcdeabcde"]}
78 |
79 | assert inspect('~ts~ts~ts', ["abcdeabcde", "abcde", "abcde"]) ==
80 | {'~ts~ts~ts', ["abcdeabcde", "", ""]}
81 | end
82 |
83 | test "timestamp" do
84 | assert {{_, _, _}, {_, _, _, _}} = timestamp(true)
85 | end
86 |
87 | test "format_date" do
88 | date = {2015, 1, 30}
89 | assert format_date(date) == ["2015", ?-, [?0, "1"], ?-, "30"]
90 | end
91 |
92 | test "format_time" do
93 | time = {12, 30, 10, 1}
94 | assert format_time(time) == ["12", ?:, "30", ?:, "10", ?., [?0, ?0, "1"]]
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/test/logger_test.exs:
--------------------------------------------------------------------------------
1 | defmodule LoggerTest do
2 | use Logger.Case
3 | require Logger
4 |
5 | test "add_translator/1 and remove_translator/1" do
6 | defmodule CustomTranslator do
7 | def t(:debug, :info, :format, {'hello: ~p', [:ok]}) do
8 | :skip
9 | end
10 |
11 | def t(:debug, :info, :format, {'world: ~p', [:ok]}) do
12 | {:ok, "rewritten"}
13 | end
14 |
15 | def t(_, _, _, _) do
16 | :none
17 | end
18 | end
19 |
20 | assert Logger.add_translator({CustomTranslator, :t})
21 |
22 | assert capture_log(fn ->
23 | :error_logger.info_msg('hello: ~p', [:ok])
24 | end) == ""
25 |
26 | assert capture_log(fn ->
27 | :error_logger.info_msg('world: ~p', [:ok])
28 | end) =~ "\[info\] rewritten"
29 | after
30 | assert Logger.remove_translator({CustomTranslator, :t})
31 | end
32 |
33 | test "add_backend/1 and remove_backend/1" do
34 | assert :ok = Logger.remove_backend(:console)
35 | assert Logger.remove_backend(:console) ==
36 | {:error, :not_found}
37 |
38 | assert capture_log(fn ->
39 | assert Logger.debug("hello", []) == :ok
40 | end) == ""
41 |
42 | assert {:ok, pid} = Logger.add_backend(:console)
43 | assert Logger.add_backend(:console) ==
44 | {:error, {:already_started, pid}}
45 | end
46 |
47 | test "level/0" do
48 | assert Logger.level == :debug
49 | end
50 |
51 | test "compare_levels/2" do
52 | assert Logger.compare_levels(:debug, :debug) == :eq
53 | assert Logger.compare_levels(:debug, :info) == :lt
54 | assert Logger.compare_levels(:debug, :warn) == :lt
55 | assert Logger.compare_levels(:debug, :error) == :lt
56 |
57 | assert Logger.compare_levels(:info, :debug) == :gt
58 | assert Logger.compare_levels(:info, :info) == :eq
59 | assert Logger.compare_levels(:info, :warn) == :lt
60 | assert Logger.compare_levels(:info, :error) == :lt
61 |
62 | assert Logger.compare_levels(:warn, :debug) == :gt
63 | assert Logger.compare_levels(:warn, :info) == :gt
64 | assert Logger.compare_levels(:warn, :warn) == :eq
65 | assert Logger.compare_levels(:warn, :error) == :lt
66 |
67 | assert Logger.compare_levels(:error, :debug) == :gt
68 | assert Logger.compare_levels(:error, :info) == :gt
69 | assert Logger.compare_levels(:error, :warn) == :gt
70 | assert Logger.compare_levels(:error, :error) == :eq
71 | end
72 |
73 | test "debug/2" do
74 | assert capture_log(fn ->
75 | assert Logger.debug("hello", []) == :ok
76 | end) =~ msg("[debug] hello")
77 |
78 | assert capture_log(:info, fn ->
79 | assert Logger.debug("hello", []) == :ok
80 | end) == ""
81 | end
82 |
83 | test "info/2" do
84 | assert capture_log(fn ->
85 | assert Logger.info("hello", []) == :ok
86 | end) =~ msg("[info] hello")
87 |
88 | assert capture_log(:warn, fn ->
89 | assert Logger.info("hello", []) == :ok
90 | end) == ""
91 | end
92 |
93 | test "warn/2" do
94 | assert capture_log(fn ->
95 | assert Logger.warn("hello", []) == :ok
96 | end) =~ msg("[warn] hello")
97 |
98 | assert capture_log(:error, fn ->
99 | assert Logger.warn("hello", []) == :ok
100 | end) == ""
101 | end
102 |
103 | test "error/2" do
104 | assert capture_log(fn ->
105 | assert Logger.error("hello", []) == :ok
106 | end) =~ msg("[error] hello")
107 | end
108 |
109 | test "remove unused calls at compile time" do
110 | Logger.configure(compile_time_purge_level: :info)
111 |
112 | defmodule Sample do
113 | def debug do
114 | Logger.debug "hello"
115 | end
116 |
117 | def info do
118 | Logger.info "hello"
119 | end
120 | end
121 |
122 | assert capture_log(fn ->
123 | assert Sample.debug == :ok
124 | end) == ""
125 |
126 | assert capture_log(fn ->
127 | assert Sample.info == :ok
128 | end) =~ msg("[info] hello")
129 | after
130 | Logger.configure(compile_time_purge_level: :debug)
131 | end
132 |
133 | test "log/2 truncates messages" do
134 | Logger.configure(truncate: 4)
135 | assert capture_log(fn ->
136 | Logger.log(:debug, "hello")
137 | end) =~ "hell (truncated)"
138 | after
139 | Logger.configure(truncate: 8096)
140 | end
141 |
142 | test "log/2 fails when the application is off" do
143 | logger = Process.whereis(Logger)
144 | Process.unregister(Logger)
145 |
146 | try do
147 | assert_raise RuntimeError,
148 | "Cannot log messages, the :logger application is not running", fn ->
149 | Logger.log(:debug, "hello")
150 | end
151 | after
152 | Process.register(logger, Logger)
153 | end
154 | end
155 |
156 | test "Logger.Config survives Logger exit" do
157 | Process.whereis(Logger)
158 | |> Process.exit(:kill)
159 | wait_for_logger()
160 | wait_for_handler(Logger, Logger.Config)
161 | end
162 |
163 | test "Logger.Config can restart the application" do
164 | Application.put_env(:logger, :backends, [])
165 | Logger.Config.restart()
166 |
167 | assert capture_log(fn ->
168 | assert Logger.debug("hello", []) == :ok
169 | end) == ""
170 |
171 | assert {:ok, pid} = Logger.add_backend(:console)
172 | assert Logger.add_backend(:console) ==
173 | {:error, {:already_started, pid}}
174 | end
175 | end
176 |
--------------------------------------------------------------------------------
/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 GenEvent.which_handlers(manager) do
19 | :timer.sleep(10)
20 | wait_for_handler(manager, handler)
21 | end
22 | end
23 |
24 | def wait_for_logger() do
25 | try do
26 | GenEvent.which_handlers(Logger)
27 | else
28 | _ ->
29 | :ok
30 | catch
31 | :exit, _ ->
32 | :timer.sleep(10)
33 | wait_for_logger()
34 | end
35 | end
36 |
37 | def capture_log(level \\ :debug, fun) do
38 | Logger.configure(level: level)
39 | capture_io(:user, fn ->
40 | fun.()
41 | GenEvent.which_handlers(:error_logger)
42 | GenEvent.which_handlers(Logger)
43 | end)
44 | after
45 | Logger.configure(level: :debug)
46 | end
47 | end
48 |
--------------------------------------------------------------------------------