├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config └── config.exs ├── lib └── lager_logger.ex ├── mix.exs ├── mix.lock └── test ├── lager_logger_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /doc 4 | /log 5 | erl_crash.dump 6 | *.ez 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.1.1 4 | - 1.2.6 5 | - 1.3.1 6 | otp_release: 7 | - 17.5 8 | - 18.2.1 9 | matrix: 10 | exclude: 11 | - elixir: 1.2.6 12 | otp_release: 17.5 13 | - elixir: 1.3.1 14 | otp_release: 17.5 15 | sudo: false # to use faster container based build environment 16 | script: 17 | - mix test 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 PSPDFKit 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LagerLogger 2 | =========== 3 | 4 | [![Build Status](https://travis-ci.org/PSPDFKit-labs/lager_logger.svg?branch=master)](https://travis-ci.org/PSPDFKit-labs/lager_logger) 5 | 6 | A [lager](https://github.com/basho/lager) backend that forwards all log messages to Elixir's [Logger](http://elixir-lang.org/docs/stable/logger/). 7 | 8 | Built by [PSPDFKit](https://pspdfkit.com) because [exometer](https://github.com/feuerlabs/exometer) has a hard dependency on lager. 9 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Stop lager redirecting :error_logger messages 4 | config :lager, :error_logger_redirect, false 5 | 6 | # Stop lager removing Logger's :error_logger handler 7 | config :lager, :error_logger_whitelist, [Logger.ErrorHandler] 8 | 9 | # Stop lager writing a crash log 10 | config :lager, :crash_log, false 11 | 12 | # Use LagerLogger as lager's only handler. 13 | config :lager, :handlers, [{LagerLogger, [level: :debug]}] 14 | -------------------------------------------------------------------------------- /lib/lager_logger.ex: -------------------------------------------------------------------------------- 1 | defmodule LagerLogger do 2 | @moduledoc ~S""" 3 | A lager backend that forwards all log messages to Elixir's Logger. 4 | 5 | To forward all lager messages to Logger and otherwise disable lager 6 | include the following in a config.exs file: 7 | 8 | use Mix.Config 9 | 10 | # Stop lager redirecting :error_logger messages 11 | config :lager, :error_logger_redirect, false 12 | 13 | # Stop lager removing Logger's :error_logger handler 14 | config :lager, :error_logger_whitelist, [Logger.ErrorHandler] 15 | 16 | # Stop lager writing a crash log 17 | config :lager, :crash_log, false 18 | 19 | # Use LagerLogger as lager's only handler. 20 | config :lager, :handlers, [{LagerLogger, [level: :debug]}] 21 | """ 22 | 23 | use Bitwise 24 | 25 | @behaviour :gen_event 26 | 27 | @doc """ 28 | Flushes lager and Logger 29 | 30 | Guarantees that all messages sent to `:error_logger` and `:lager`, prior to 31 | this call, have been handled by Logger. 32 | """ 33 | @spec flush() :: :ok 34 | def flush() do 35 | _ = GenEvent.which_handlers(:error_logger) 36 | _ = GenEvent.which_handlers(:lager_event) 37 | _ = GenEvent.which_handlers(Logger) 38 | :ok 39 | end 40 | 41 | @doc false 42 | def init(opts) do 43 | config = Keyword.get(opts, :level, :debug) 44 | case config_to_mask(config) do 45 | {:ok, _mask} = ok -> 46 | ok 47 | {:error, reason} -> 48 | {:error, {:fatal, reason}} 49 | end 50 | end 51 | 52 | @doc false 53 | def handle_event({:log, lager_msg}, mask) do 54 | %{mode: mode, truncate: truncate, level: min_level, utc_log: utc_log?} = Logger.Config.__data__ 55 | level = severity_to_level(:lager_msg.severity(lager_msg)) 56 | 57 | if :lager_util.is_loggable(lager_msg, mask, __MODULE__) and 58 | Logger.compare_levels(level, min_level) != :lt do 59 | 60 | metadata = :lager_msg.metadata(lager_msg) |> normalize_pid 61 | 62 | # lager_msg's message is already formatted chardata 63 | message = Logger.Utils.truncate(:lager_msg.message(lager_msg), truncate) 64 | 65 | # Lager always uses local time and converts it when formatting using :lager_util.maybe_utc 66 | timestamp = timestamp(:lager_msg.timestamp(lager_msg), utc_log?) 67 | 68 | group_leader = case Keyword.fetch(metadata, :pid) do 69 | {:ok, pid} when is_pid(pid) -> 70 | case Process.info(pid, :group_leader) do 71 | {:group_leader, gl} -> gl 72 | nil -> Process.group_leader # if pid dead, pretend it's us as must be a pid 73 | end 74 | _ -> Process.group_leader # if lager didn't give us a pid just pretend it's us 75 | end 76 | 77 | _ = notify(mode, {level, group_leader, {Logger, message, timestamp, metadata}}) 78 | {:ok, mask} 79 | else 80 | {:ok, mask} 81 | end 82 | end 83 | 84 | @doc false 85 | def handle_call(:get_loglevel, mask) do 86 | {:ok, mask, mask} 87 | end 88 | 89 | def handle_call({:set_loglevel, config}, mask) do 90 | case config_to_mask(config) do 91 | {:ok, mask} -> 92 | {:ok, :ok, mask} 93 | {:error, _reason} = error -> 94 | {:ok, error, mask} 95 | end 96 | end 97 | 98 | @doc false 99 | def handle_info(_msg, mask) do 100 | {:ok, mask} 101 | end 102 | 103 | @doc false 104 | def terminate(_reason, _mask), do: :ok 105 | 106 | @doc false 107 | def code_change(_old, mask, _extra), do: {:ok, mask} 108 | 109 | defp config_to_mask(config) do 110 | try do 111 | :lager_util.config_to_mask(config) 112 | catch 113 | _, _ -> 114 | {:error, {:bad_log_level, config}} 115 | else 116 | mask -> 117 | {:ok, mask} 118 | end 119 | end 120 | 121 | # Stolen from Logger. 122 | defp notify(:sync, msg), do: GenEvent.sync_notify(Logger, msg) 123 | defp notify(:async, msg), do: GenEvent.notify(Logger, msg) 124 | 125 | @doc false 126 | # Lager's parse transform converts the pid into a charlist. Logger's metadata expects pids as 127 | # actual pids so we need to revert it. 128 | # If the pid metadata is not a valid pid we remove it completely. 129 | def normalize_pid(metadata) do 130 | case Keyword.fetch(metadata, :pid) do 131 | {:ok, pid} when is_pid(pid) -> metadata 132 | {:ok, pid} when is_list(pid) -> 133 | try do 134 | # Lager's parse transform uses `pid_to_list` so we revert it 135 | Keyword.put(metadata, :pid, :erlang.list_to_pid(pid)) 136 | rescue 137 | ArgumentError -> Keyword.delete(metadata, :pid) 138 | end 139 | {:ok, _} -> Keyword.delete(metadata, :pid) 140 | :error -> metadata 141 | end 142 | end 143 | 144 | @doc false 145 | # Returns a timestamp that includes miliseconds. Stolen from Logger.Utils. 146 | def timestamp(now, utc_log?) do 147 | {_, _, micro} = now 148 | {date, {hours, minutes, seconds}} = 149 | case utc_log? do 150 | true -> :calendar.now_to_universal_time(now) 151 | false -> :calendar.now_to_local_time(now) 152 | end 153 | {date, {hours, minutes, seconds, div(micro, 1000)}} 154 | end 155 | 156 | # Converts lager's severity to Logger's level 157 | defp severity_to_level(:debug), do: :debug 158 | defp severity_to_level(:info), do: :info 159 | defp severity_to_level(:notice), do: :info 160 | defp severity_to_level(:warning), do: :warn 161 | defp severity_to_level(:error), do: :error 162 | defp severity_to_level(:critical), do: :error 163 | defp severity_to_level(:alert), do: :error 164 | defp severity_to_level(:emergency), do: :error 165 | end 166 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LagerLogger.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :lager_logger, 6 | version: "1.0.5", 7 | elixir: ">= 1.1.0 and < 1.7.0", 8 | package: package(), 9 | description: description(), 10 | deps: deps()] 11 | end 12 | 13 | defp package do 14 | [maintainers: ["Martin Schurrer", "James Fish"], 15 | licenses: ["Apache 2.0"], 16 | links: %{"GitHub" => "https://github.com/PSPDFKit-labs/lager_logger"}, 17 | files: ["lib", "mix.exs", "README.md"]] 18 | end 19 | 20 | defp description do 21 | """ 22 | LagerLogger is a lager backend that forwards all log messages to Elixir's Logger. 23 | """ 24 | end 25 | 26 | 27 | def application do 28 | [applications: [:lager, :logger]] 29 | end 30 | 31 | defp deps do 32 | [ 33 | {:ex_doc, ">= 0.0.0", only: :dev}, 34 | {:lager, ">= 2.1.0"}, 35 | ] 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "1.1.0", "8c2bf85d725050a92042bc1edf362621004d43ca6241c756f39612084e95487f", [:mix], []}, 2 | "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, 3 | "goldrush": {:hex, :goldrush, "0.1.6", "8e224150c6994cc908bfee63c55e60262edbc0c60ee618b0b90bc3167a0d50c7", [:make, :rebar], []}, 4 | "lager": {:hex, :lager, "2.1.1", "8e389f916384abf5f62169969bd7515c4c252cbbda53feebbef90f47db5eaad1", [:make, :rebar], [{:goldrush, "~> 0.1.6", [hex: :goldrush, optional: false]}]}} 5 | -------------------------------------------------------------------------------- /test/lager_logger_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LagerLoggerTest do 2 | use LagerLogger.Case 3 | 4 | alias LagerLogger, as: L 5 | 6 | setup_all do 7 | stop([:lager]) 8 | on_exit(fn() -> Application.start(:lager) end) 9 | :ok 10 | end 11 | 12 | test "forward all messages at lager level debug and Logger level debug" do 13 | lager_env = [handlers: [{LagerLogger, [level: :debug]}]] 14 | level = :debug 15 | 16 | assert capture_log(lager_env, level, fn() -> 17 | :lager.log(:debug, self(), 'hello') 18 | end) =~ "[debug] hello" 19 | 20 | assert capture_log(lager_env, level, fn() -> 21 | :lager.log(:info, self(), 'hello') 22 | end) =~ "[info] hello" 23 | 24 | assert capture_log(lager_env, level, fn() -> 25 | :lager.log(:notice, self(), 'hello') 26 | end) =~ "[info] hello" 27 | 28 | assert capture_log(lager_env, level, fn() -> 29 | :lager.log(:warning, self(), 'hello') 30 | end) =~ "[warn] hello" 31 | 32 | assert capture_log(lager_env, level, fn() -> 33 | :lager.log(:error, self(), 'hello') 34 | end) =~ "[error] hello" 35 | 36 | assert capture_log(lager_env, level, fn() -> 37 | :lager.log(:critical, self(), 'hello') 38 | end) =~ "[error] hello" 39 | 40 | assert capture_log(lager_env, level, fn() -> 41 | :lager.log(:alert, self(), 'hello') 42 | end) =~ "[error] hello" 43 | 44 | assert capture_log(lager_env, level, fn() -> 45 | :lager.log(:emergency, self(), 'hello') 46 | end) =~ "[error] hello" 47 | end 48 | 49 | test "forward all but debug lager messages at lager level :info" do 50 | lager_env = [handlers: [{LagerLogger, [level: :info]}]] 51 | level = :debug 52 | 53 | assert capture_log(lager_env, level, fn() -> 54 | :lager.log(:debug, self(), 'hello') 55 | end) == "" 56 | 57 | assert capture_log(lager_env, level, fn() -> 58 | :lager.log(:info, self(), 'hello') 59 | end) =~ "[info] hello" 60 | 61 | assert capture_log(lager_env, level, fn() -> 62 | :lager.log(:alert, self(), 'hello') 63 | end) =~ "[error] hello" 64 | end 65 | 66 | test "forward debug and >info lager messages at lager level !=info" do 67 | lager_env = [handlers: [{LagerLogger, [level: :"!=info"]}]] 68 | level = :debug 69 | 70 | assert capture_log(lager_env, level, fn() -> 71 | :lager.log(:debug, self(), 'hello') 72 | end) =~ "[debug] hello" 73 | 74 | assert capture_log(lager_env, level, fn() -> 75 | :lager.log(:info, self(), 'hello') 76 | end) == "" 77 | 78 | assert capture_log(lager_env, level, fn() -> 79 | :lager.log(:notice, self(), 'hello') 80 | end) =~ "[info] hello" 81 | 82 | assert capture_log(lager_env, level, fn() -> 83 | :lager.log(:alert, self(), 'hello') 84 | end) =~ "[error] hello" 85 | end 86 | 87 | test "forward >= warning lager messages at Logger level warn" do 88 | lager_env = [handlers: [{LagerLogger, [level: :debug]}]] 89 | level = :warn 90 | 91 | assert capture_log(lager_env, level, fn() -> 92 | :lager.log(:debug, self(), 'hello') 93 | end) == "" 94 | 95 | assert capture_log(lager_env, level, fn() -> 96 | :lager.log(:notice, self(), 'hello') 97 | end) == "" 98 | 99 | assert capture_log(lager_env, level, fn() -> 100 | :lager.log(:warning, self(), 'hello') 101 | end) =~ "[warn] hello" 102 | 103 | assert capture_log(lager_env, level, fn() -> 104 | :lager.log(:critical, self(), 'hello') 105 | end) =~ "[error] hello" 106 | end 107 | 108 | test "normalize_pid with metadata containing a pid" do 109 | metadata = [a: 1, pid: self(), b: 2] 110 | assert Keyword.equal?(L.normalize_pid(metadata), metadata) 111 | end 112 | 113 | test "normalize_pid with normal metadata" do 114 | metadata = [a: 1, b: 2] 115 | assert Keyword.equal?(L.normalize_pid(metadata), metadata) 116 | end 117 | 118 | test "normalize_pid with metadata containing a valid pid as charlist" do 119 | metadata = [a: 1, pid: :erlang.pid_to_list(self()), b: 2] 120 | assert Keyword.equal?(L.normalize_pid(metadata), [a: 1, pid: self(), b: 2]) 121 | end 122 | 123 | test "normalize_pid with metadata containing an invalid pid" do 124 | assert Keyword.equal?(L.normalize_pid([a: 1, pid: 'lol', b: 2]), [a: 1, b: 2]) 125 | assert Keyword.equal?(L.normalize_pid([a: 1, pid: "not even a charlist", b: 2]), [a: 1, b: 2]) 126 | end 127 | 128 | test "timestamp" do 129 | assert {{_, _, _}, {_, _, _, _}} = L.timestamp(:os.timestamp, true) 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule LagerLogger.Case do 4 | use ExUnit.CaseTemplate 5 | 6 | using _ do 7 | quote do 8 | import LagerLogger.Case 9 | end 10 | end 11 | 12 | # stop apps without logging to console 13 | def stop(apps) do 14 | Logger.flush() 15 | :ok = Logger.remove_backend(:console) 16 | _ = for app <- apps, do: Application.stop(app) 17 | Logger.flush() 18 | {:ok, _} = Logger.add_backend(:console) 19 | :ok 20 | end 21 | 22 | def configure(env) do 23 | _ = for {key, value} <- env do 24 | :ok = Application.put_env(:lager, key, value) 25 | end 26 | :ok 27 | end 28 | 29 | # Restart lager with new config, set Logger level, capture all Logger logs, 30 | # stop lager and then reset Logger level 31 | def capture_log(lager_env, level, fun) do 32 | stop([:lager]) 33 | prior_lager_env = :application.get_all_env(:lager) 34 | prior_level = Logger.level() 35 | try do 36 | Logger.configure([level: level]) 37 | configure(lager_env) 38 | :ok = Application.start(:lager) 39 | ExUnit.CaptureIO.capture_io(:user, fn -> 40 | fun.() 41 | LagerLogger.flush() 42 | end) 43 | after 44 | stop([:lager]) 45 | _ = for {key, _} <- lager_env, do: Application.delete_env(:lager, key) 46 | configure(prior_lager_env) 47 | Logger.configure([level: prior_level]) 48 | end 49 | end 50 | end 51 | --------------------------------------------------------------------------------