├── .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 | --------------------------------------------------------------------------------