├── .formatter.exs ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── assets └── logo.png ├── config ├── config.exs ├── dev.exs └── test.exs ├── lib └── logger_file_backend.ex ├── mix.exs ├── mix.lock └── test ├── logger_file_backend_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | logger_file_backend-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## v0.0.14 11 | 12 | - fix warnings about the use of deprecated :warn 13 | - Bumps versions of `credo` and `ex_doc` to latest 14 | 15 | ## v0.0.13 16 | 17 | - Docs cleanup 18 | - Release file handle 19 | - Bumps versions of `credo` and `ex_doc` to latest 20 | 21 | ## v0.0.12 - 2021-07-19 22 | 23 | - Bumps dependency versions 24 | - Combing through documentation 25 | 26 | ## v0.0.11 - 2019-08-06 27 | 28 | ### Enhancements 29 | 30 | - Add simple strategy to rotate log files ([3a4d7f](https://github.com/mstratman/logger_file_backend/commit/3a4d7ffea4fd1ea4f4ba2629051efc259dd668ec)) 31 | - Allow :all in :metadata option ([#54](https://github.com/onkel-dirtus/logger_file_backend/pull/54)) 32 | 33 | ### Fixes 34 | 35 | - Documentation fix ([#39](https://github.com/onkel-dirtus/logger_file_backend/pull/39)) 36 | - Eliminate warnings ([#30](https://github.com/onkel-dirtus/logger_file_backend/pull/30), [361c7d](https://github.com/mstratman/logger_file_backend/commit/361c7d81cb408a8aee824d080e16fd26f1920621), [1f0390](https://github.com/mstratman/logger_file_backend/commit/1f0390b29fe90516bd4b70d82250de065900fd41), [#43](https://github.com/onkel-dirtus/logger_file_backend/pull/43)) 37 | 38 | ### Changes 39 | 40 | - Update dependencies ([#30](https://github.com/onkel-dirtus/logger_file_backend/pull/30)) 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Kurt Williams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LoggerFileBackend 2 | 3 | [![Module Version](https://img.shields.io/hexpm/v/logger_file_backend.svg)](https://hex.pm/packages/logger_file_backend) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/logger_file_backend/) 5 | [![Total Download](https://img.shields.io/hexpm/dt/logger_file_backend.svg)](https://hex.pm/packages/logger_file_backend) 6 | [![License](https://img.shields.io/hexpm/l/logger_file_backend.svg)](https://github.com/onkel-dirtus/logger_file_backend/blob/master/LICENSE.md) 7 | [![Last Updated](https://img.shields.io/github/last-commit/onkel-dirtus/logger_file_backend.svg)](https://github.com/onkel-dirtus/logger_file_backend/commits/master) 8 | 9 | A simple Elixir `Logger` backend which writes logs to a file. It does not handle log rotation, but it does tolerate log file renames, so it can be used in conjunction with external log rotation. 10 | 11 | **Note** The renaming of log files does not work on Windows, because `File.Stat.inode` is used to determine whether the log file has been (re)moved and, on non-Unix, `File.Stat.inode` is always 0. 12 | 13 | **Note** If you are running this with the Phoenix framework, please review the Phoenix specific instructions later on in this file. 14 | 15 | ## Configuration 16 | 17 | `LoggerFileBackend` is a custom backend for the elixir `:logger` application. As 18 | such, it relies on the `:logger` application to start the relevant processes. 19 | However, unlike the default `:console` backend, we may want to configure 20 | multiple log files, each with different log levels, formats, etc. Also, we want 21 | `:logger` to be responsible for starting and stopping each of our logging 22 | processes for us. Because of these considerations, there must be one `:logger` 23 | backend configured for each log file we need. Each backend has a name like 24 | `{LoggerFileBackend, id}`, where `id` is any elixir term (usually an atom). 25 | 26 | For example, let's say we want to log error messages to 27 | `"/var/log/my_app/error.log"`. To do that, we will need to configure a backend. 28 | Let's call it `{LoggerFileBackend, :error_log}`. 29 | 30 | Our `config.exs` would have an entry similar to this: 31 | 32 | ```elixir 33 | # tell logger to load a LoggerFileBackend processes 34 | config :logger, 35 | backends: [{LoggerFileBackend, :error_log}] 36 | ``` 37 | 38 | With this configuration, the `:logger` application will start one `LoggerFileBackend` 39 | named `{LoggerFileBackend, :error_log}`. We still need to set the correct file 40 | path and log levels for the backend, though. To do that, we add another config 41 | stanza. Together with the stanza above, we'll have something like this: 42 | 43 | ```elixir 44 | # tell logger to load a LoggerFileBackend processes 45 | config :logger, 46 | backends: [{LoggerFileBackend, :error_log}] 47 | 48 | # configuration for the {LoggerFileBackend, :error_log} backend 49 | config :logger, :error_log, 50 | path: "/var/log/my_app/error.log", 51 | level: :error 52 | ``` 53 | 54 | Check out the examples below for runtime configuration and configuration for 55 | multiple log files. 56 | 57 | `LoggerFileBackend` supports the following configuration values: 58 | 59 | * `path` - the path to the log file 60 | * `level` - the logging level for the backend 61 | * `format` - the logging format for the backend 62 | * `metadata` - the metadata to include 63 | * `metadata_filter` - metadata terms which must be present in order to log 64 | * metadata_reject - metadata terms which must be present in order to do not log 65 | 66 | ### Examples 67 | 68 | #### Runtime configuration 69 | 70 | ```elixir 71 | Logger.add_backend {LoggerFileBackend, :debug} 72 | Logger.configure_backend {LoggerFileBackend, :debug}, 73 | path: "/path/to/debug.log", 74 | format: ..., 75 | metadata: ..., 76 | metadata_filter: ... 77 | ``` 78 | 79 | #### Application config for multiple log files 80 | 81 | ```elixir 82 | config :logger, 83 | backends: [{LoggerFileBackend, :info}, 84 | {LoggerFileBackend, :error}] 85 | 86 | config :logger, :info, 87 | path: "/path/to/info.log", 88 | level: :info 89 | 90 | config :logger, :error, 91 | path: "/path/to/error.log", 92 | level: :error 93 | ``` 94 | 95 | #### Filtering specific metadata terms 96 | 97 | This example only logs `:info` statements originating from the `:ui` OTP app; the `:application` metadata key is auto-populated by `Logger`. 98 | 99 | ```elixir 100 | config :logger, 101 | backends: [{LoggerFileBackend, :ui}] 102 | 103 | config :logger, :ui, 104 | path: "/path/to/ui.log", 105 | level: :info, 106 | metadata_filter: [application: :ui] 107 | ``` 108 | 109 | This example only writes log statements with a custom metadata key to the file. 110 | 111 | ```elixir 112 | # in a config file: 113 | config :logger, 114 | backends: [{LoggerFileBackend, :device_1}] 115 | 116 | config :logger, :device_1, 117 | path: "/path/to/device_1.log", 118 | level: :debug, 119 | metadata_filter: [device: 1] 120 | 121 | # Usage: 122 | # anywhere in the code: 123 | Logger.info("statement", device: 1) 124 | 125 | # or, for a single process, e.g., a GenServer: 126 | # in init/1: 127 | Logger.metadata(device: 1) 128 | # ^ sets device: 1 for all subsequent log statements from this process. 129 | 130 | # Later, in other code (handle_cast/2, etc.) 131 | Logger.info("statement") # <= already tagged with the device_1 metadata 132 | ``` 133 | 134 | ## Additional Phoenix Configurations 135 | 136 | Phoenix makes use of its own `mix.exs` file to track dependencies and additional applications. Add the following to your `mix.exs`: 137 | 138 | ```elixir 139 | def application do 140 | [applications: [ 141 | ..., 142 | :logger_file_backend, 143 | ... 144 | ] 145 | ] 146 | end 147 | 148 | defp deps do 149 | [ ... 150 | {:logger_file_backend, "~> 0.0.10"}, 151 | ] 152 | end 153 | ``` 154 | 155 | ## Copyright and License 156 | 157 | Copyright (c) 2014 Kurt Williams 158 | 159 | This library licensed under the [MIT license](./LICENSE.md). 160 | 161 | ## Image Attribution 162 | 163 | "log" by Matthew Weatherall from [the Noun Project](https://thenounproject.com/). 164 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onkel-dirtus/logger_file_backend/a2e9339abefd4de24b0a0a09c0c7c58299758277/assets/logo.png -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, 14 | # level: :info, 15 | # format: "$time $metadata[$level] $message\n" 16 | 17 | # It is also possible to import configuration files, relative to this 18 | # directory. For example, you can emulate configuration per environment 19 | # by uncommenting the line below and defining dev.exs, test.exs and such. 20 | # Configuration from the imported file will override the ones defined 21 | # here (which is why it is important to import them last). 22 | # 23 | 24 | import_config "#{Mix.env()}.exs" 25 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, 4 | backends: [{LoggerFileBackend, :dev_backend}], 5 | level: :info, 6 | format: "$time $metadata[$level] $message\n" 7 | 8 | config :logger, :dev_backend, 9 | level: :error, 10 | path: "test/logs/error.log", 11 | format: "DEV $message" 12 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, backends: [] 4 | 5 | # config :logger, :test, 6 | # level: :debug, 7 | # path: "test/logs/error.log" 8 | -------------------------------------------------------------------------------- /lib/logger_file_backend.ex: -------------------------------------------------------------------------------- 1 | defmodule LoggerFileBackend do 2 | @moduledoc """ 3 | `LoggerFileBackend` is a custom backend for the elixir `:logger` application. 4 | """ 5 | 6 | @behaviour :gen_event 7 | 8 | @type path :: String.t() 9 | @type file :: :file.io_device() 10 | @type inode :: integer 11 | @type format :: String.t() 12 | @type level :: Logger.level() 13 | @type metadata :: [atom] 14 | 15 | require Record 16 | Record.defrecordp(:file_info, Record.extract(:file_info, from_lib: "kernel/include/file.hrl")) 17 | 18 | @default_format "$time $metadata[$level] $message\n" 19 | 20 | def init({__MODULE__, name}) do 21 | {:ok, configure(name, [])} 22 | end 23 | 24 | def handle_call({:configure, opts}, %{name: name} = state) do 25 | {:ok, :ok, configure(name, opts, state)} 26 | end 27 | 28 | def handle_call(:path, %{path: path} = state) do 29 | {:ok, {:ok, path}, state} 30 | end 31 | 32 | def handle_event( 33 | {level, _gl, {Logger, msg, ts, md}}, 34 | %{level: min_level, metadata_filter: metadata_filter, metadata_reject: metadata_reject} = 35 | state 36 | ) do 37 | level = to_logger_level(level) 38 | min_level = to_logger_level(min_level) 39 | 40 | if (is_nil(min_level) or Logger.compare_levels(level, min_level) != :lt) and 41 | metadata_matches?(md, metadata_filter) and 42 | (is_nil(metadata_reject) or !metadata_matches?(md, metadata_reject)) do 43 | log_event(level, msg, ts, md, state) 44 | else 45 | {:ok, state} 46 | end 47 | end 48 | 49 | def handle_event(:flush, state) do 50 | # We're not buffering anything so this is a no-op 51 | {:ok, state} 52 | end 53 | 54 | def handle_info({:EXIT, _pid, _reason}, %{io_device: io_device} = state) 55 | when not is_nil(io_device) do 56 | case File.close(io_device) do 57 | :ok -> {:ok, state} 58 | {:error, reason} -> raise "failure while closing file for reason: #{reason}" 59 | end 60 | end 61 | 62 | def handle_info(_, state) do 63 | {:ok, state} 64 | end 65 | 66 | # helpers 67 | 68 | defp log_event(_level, _msg, _ts, _md, %{path: nil} = state) do 69 | {:ok, state} 70 | end 71 | 72 | defp log_event(level, msg, ts, md, %{path: path, io_device: nil} = state) 73 | when is_binary(path) do 74 | case open_log(path) do 75 | {:ok, io_device, inode} -> 76 | log_event(level, msg, ts, md, %{state | io_device: io_device, inode: inode}) 77 | 78 | _other -> 79 | {:ok, state} 80 | end 81 | end 82 | 83 | defp log_event( 84 | level, 85 | msg, 86 | ts, 87 | md, 88 | %{path: path, io_device: io_device, inode: inode, rotate: rotate} = state 89 | ) 90 | when is_binary(path) do 91 | if !is_nil(inode) and inode == get_inode(path) and rotate(path, rotate) do 92 | output = format_event(level, msg, ts, md, state) 93 | 94 | try do 95 | IO.write(io_device, output) 96 | {:ok, state} 97 | rescue 98 | ErlangError -> 99 | case open_log(path) do 100 | {:ok, io_device, inode} -> 101 | IO.write(io_device, prune(output)) 102 | {:ok, %{state | io_device: io_device, inode: inode}} 103 | 104 | _other -> 105 | {:ok, %{state | io_device: nil, inode: nil}} 106 | end 107 | end 108 | else 109 | File.close(io_device) 110 | log_event(level, msg, ts, md, %{state | io_device: nil, inode: nil}) 111 | end 112 | end 113 | 114 | defp rename_file(path, keep) do 115 | File.rm("#{path}.#{keep}") 116 | 117 | Enum.each((keep - 1)..1, fn x -> File.rename("#{path}.#{x}", "#{path}.#{x + 1}") end) 118 | 119 | case File.rename(path, "#{path}.1") do 120 | :ok -> false 121 | _ -> true 122 | end 123 | end 124 | 125 | defp rotate(path, %{max_bytes: max_bytes, keep: keep}) 126 | when is_integer(max_bytes) and is_integer(keep) and keep > 0 do 127 | case :file.read_file_info(path, [:raw]) do 128 | {:ok, file_info(size: size)} -> 129 | if size >= max_bytes, do: rename_file(path, keep), else: true 130 | 131 | _ -> 132 | true 133 | end 134 | end 135 | 136 | defp rotate(_path, nil), do: true 137 | 138 | defp open_log(path) do 139 | case path |> Path.dirname() |> File.mkdir_p() do 140 | :ok -> 141 | case File.open(path, [:append, :utf8]) do 142 | {:ok, io_device} -> {:ok, io_device, get_inode(path)} 143 | other -> other 144 | end 145 | 146 | other -> 147 | other 148 | end 149 | end 150 | 151 | defp format_event(level, msg, ts, md, %{format: format, metadata: keys}) do 152 | Logger.Formatter.format(format, level, msg, ts, take_metadata(md, keys)) 153 | end 154 | 155 | @doc false 156 | @spec metadata_matches?(Keyword.t(), nil | Keyword.t()) :: true | false 157 | def metadata_matches?(_md, nil), do: true 158 | # all of the filter keys are present 159 | def metadata_matches?(_md, []), do: true 160 | 161 | def metadata_matches?(md, [{key, [_ | _] = val} | rest]) do 162 | case Keyword.fetch(md, key) do 163 | {:ok, md_val} -> 164 | md_val in val && metadata_matches?(md, rest) 165 | 166 | # fail on first mismatch 167 | _ -> 168 | false 169 | end 170 | end 171 | 172 | def metadata_matches?(md, [{key, val} | rest]) do 173 | case Keyword.fetch(md, key) do 174 | {:ok, ^val} -> 175 | metadata_matches?(md, rest) 176 | 177 | # fail on first mismatch 178 | _ -> 179 | false 180 | end 181 | end 182 | 183 | defp take_metadata(metadata, :all), do: metadata 184 | 185 | defp take_metadata(metadata, keys) do 186 | metadatas = 187 | Enum.reduce(keys, [], fn key, acc -> 188 | case Keyword.fetch(metadata, key) do 189 | {:ok, val} -> [{key, val} | acc] 190 | :error -> acc 191 | end 192 | end) 193 | 194 | Enum.reverse(metadatas) 195 | end 196 | 197 | defp get_inode(path) do 198 | case :file.read_file_info(path, [:raw]) do 199 | {:ok, file_info(inode: inode)} -> inode 200 | {:error, _} -> nil 201 | end 202 | end 203 | 204 | defp configure(name, opts) do 205 | state = %{ 206 | name: nil, 207 | path: nil, 208 | io_device: nil, 209 | inode: nil, 210 | format: nil, 211 | level: nil, 212 | metadata: nil, 213 | metadata_filter: nil, 214 | metadata_reject: nil, 215 | rotate: nil 216 | } 217 | 218 | configure(name, opts, state) 219 | end 220 | 221 | defp configure(name, opts, state) do 222 | env = Application.get_env(:logger, name, []) 223 | opts = Keyword.merge(env, opts) 224 | Application.put_env(:logger, name, opts) 225 | 226 | level = Keyword.get(opts, :level) 227 | metadata = Keyword.get(opts, :metadata, []) 228 | format_opts = Keyword.get(opts, :format, @default_format) 229 | format = Logger.Formatter.compile(format_opts) 230 | path = Keyword.get(opts, :path) 231 | metadata_filter = Keyword.get(opts, :metadata_filter) 232 | metadata_reject = Keyword.get(opts, :metadata_reject) 233 | rotate = Keyword.get(opts, :rotate) 234 | 235 | %{ 236 | state 237 | | name: name, 238 | path: path, 239 | format: format, 240 | level: level, 241 | metadata: metadata, 242 | metadata_filter: metadata_filter, 243 | metadata_reject: metadata_reject, 244 | rotate: rotate 245 | } 246 | end 247 | 248 | @replacement "�" 249 | 250 | @spec prune(IO.chardata()) :: IO.chardata() 251 | def prune(binary) when is_binary(binary), do: prune_binary(binary, "") 252 | def prune([h | t]) when h in 0..1_114_111, do: [h | prune(t)] 253 | def prune([h | t]), do: [prune(h) | prune(t)] 254 | def prune([]), do: [] 255 | def prune(_), do: @replacement 256 | 257 | defp prune_binary(<>, acc), 258 | do: prune_binary(t, <>) 259 | 260 | defp prune_binary(<<_, t::binary>>, acc), 261 | do: prune_binary(t, <>) 262 | 263 | defp prune_binary(<<>>, acc), 264 | do: acc 265 | 266 | defp to_logger_level(:warn) do 267 | if Version.compare(System.version(), "1.11.0") != :lt, 268 | do: :warning, 269 | else: :warn 270 | end 271 | 272 | defp to_logger_level(level), do: level 273 | end 274 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LoggerFileBackend.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/onkel-dirtus/logger_file_backend" 5 | @version "0.0.14" 6 | 7 | def project do 8 | [ 9 | app: :logger_file_backend, 10 | version: @version, 11 | elixir: "~> 1.0", 12 | package: package(), 13 | deps: deps(), 14 | docs: docs() 15 | ] 16 | end 17 | 18 | def application do 19 | [extra_applications: [:logger]] 20 | end 21 | 22 | defp package do 23 | [ 24 | description: "Simple logger backend that writes to a file", 25 | maintainers: ["Kurt Williams", "Everett Griffiths"], 26 | licenses: ["MIT"], 27 | files: [ 28 | "lib", 29 | "assets/logo.png", 30 | "mix.exs", 31 | "README*", 32 | "CHANGELOG*", 33 | "LICENSE*" 34 | ], 35 | links: %{ 36 | "Changelog" => "https://hexdocs.pm/logger_file_backend/changelog.html", 37 | "GitHub" => @source_url 38 | } 39 | ] 40 | end 41 | 42 | defp deps do 43 | [ 44 | {:credo, "~> 1.7.5", only: [:dev, :test]}, 45 | {:ex_doc, ">= 0.32.1", only: :dev, runtime: false} 46 | ] 47 | end 48 | 49 | defp docs do 50 | [ 51 | extras: [ 52 | "CHANGELOG.md": [], 53 | "LICENSE.md": [title: "License"], 54 | "README.md": [title: "Overview"] 55 | ], 56 | main: "readme", 57 | source_url: @source_url, 58 | source_ref: "v#{@version}", 59 | logo: "assets/logo.png", 60 | formatters: ["html"] 61 | ] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 5 | "ex_doc": {:hex, :ex_doc, "0.32.1", "21e40f939515373bcdc9cffe65f3b3543f05015ac6c3d01d991874129d173420", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5142c9db521f106d61ff33250f779807ed2a88620e472ac95dc7d59c380113da"}, 6 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, 7 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 8 | "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, 9 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 10 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, 11 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 12 | } 13 | -------------------------------------------------------------------------------- /test/logger_file_backend_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LoggerFileBackendTest do 2 | use ExUnit.Case, async: false 3 | require Logger 4 | 5 | @backend {LoggerFileBackend, :test} 6 | @basedir "test/logs" 7 | 8 | import LoggerFileBackend, only: [prune: 1, metadata_matches?: 2] 9 | 10 | setup_all do 11 | on_exit(fn -> 12 | File.rm_rf!(@basedir) 13 | end) 14 | end 15 | 16 | setup context do 17 | # We add and remove the backend here to avoid cross-test effects 18 | Logger.add_backend(@backend, flush: true) 19 | 20 | config(path: logfile(context, @basedir), level: :debug) 21 | 22 | on_exit(fn -> 23 | :ok = Logger.remove_backend(@backend) 24 | end) 25 | end 26 | 27 | test "does not crash if path isn't set" do 28 | config(path: nil) 29 | 30 | Logger.debug("foo") 31 | assert {:error, :already_present} = Logger.add_backend(@backend) 32 | end 33 | 34 | test "can configure metadata_filter" do 35 | config(metadata_filter: [md_key: true]) 36 | Logger.debug("shouldn't", md_key: false) 37 | Logger.debug("should", md_key: true) 38 | refute log() =~ "shouldn't" 39 | assert log() =~ "should" 40 | config(metadata_filter: nil) 41 | end 42 | 43 | test "can configure metadata_reject" do 44 | config(metadata_reject: [md_key: false]) 45 | Logger.debug("shouldn't", md_key: false) 46 | Logger.debug("should", md_key: true) 47 | refute log() =~ "shouldn't" 48 | assert log() =~ "should" 49 | config(metadata_reject: nil) 50 | end 51 | 52 | test "metadata_matches?" do 53 | # exact match 54 | assert metadata_matches?([a: 1], a: 1) == true 55 | # included in array match 56 | assert metadata_matches?([a: 1], a: [1, 2]) == true 57 | # total mismatch 58 | assert metadata_matches?([b: 1], a: 1) == false 59 | # default to allow 60 | assert metadata_matches?([b: 1], nil) == true 61 | # metadata is superset of filter 62 | assert metadata_matches?([b: 1, a: 1], a: 1) == true 63 | # multiple filter keys subset of metadata 64 | assert metadata_matches?([c: 1, b: 1, a: 1], b: 1, a: 1) == true 65 | # multiple filter keys superset of metadata 66 | assert metadata_matches?([a: 1], b: 1, a: 1) == false 67 | end 68 | 69 | test "creates log file" do 70 | refute File.exists?(path()) 71 | Logger.debug("this is a msg") 72 | assert File.exists?(path()) 73 | assert log() =~ "this is a msg" 74 | end 75 | 76 | test "can log utf8 chars" do 77 | Logger.debug("ß\uFFaa\u0222") 78 | assert log() =~ "ßᆰȢ" 79 | end 80 | 81 | test "prune/1" do 82 | assert prune(1) == "�" 83 | assert prune(<<"hí", 233>>) == "hí�" 84 | assert prune(["hi" | 233]) == ["hi" | "�"] 85 | assert prune([233 | "hi"]) == [233 | "hi"] 86 | assert prune([[] | []]) == [[]] 87 | end 88 | 89 | test "prunes invalid utf-8 codepoints" do 90 | Logger.debug(<<"hi", 233>>) 91 | assert log() =~ "hi�" 92 | end 93 | 94 | test "can configure format" do 95 | config(format: "$message [$level]\n") 96 | 97 | Logger.debug("hello") 98 | assert log() =~ "hello [debug]" 99 | end 100 | 101 | test "can configure metadata" do 102 | config(format: "$metadata$message\n", metadata: [:user_id, :auth]) 103 | 104 | Logger.debug("hello") 105 | assert log() =~ "hello" 106 | 107 | Logger.metadata(auth: true) 108 | Logger.metadata(user_id: 11) 109 | Logger.metadata(user_id: 13) 110 | 111 | Logger.debug("hello") 112 | assert log() =~ "user_id=13 auth=true hello" 113 | end 114 | 115 | test "can configure level" do 116 | config(level: :info) 117 | 118 | Logger.debug("hello") 119 | refute File.exists?(path()) 120 | end 121 | 122 | test "can configure path" do 123 | new_path = "test/logs/test.log.2" 124 | config(path: new_path) 125 | assert new_path == path() 126 | end 127 | 128 | test "logs to new file after old file has been moved" do 129 | config(format: "$message\n") 130 | 131 | Logger.debug("foo") 132 | Logger.debug("bar") 133 | assert log() == "foo\nbar\n" 134 | 135 | {"", 0} = System.cmd("mv", [path(), path() <> ".1"]) 136 | 137 | Logger.debug("biz") 138 | Logger.debug("baz") 139 | assert log() == "biz\nbaz\n" 140 | end 141 | 142 | test "closes old log file after log file has been moved" do 143 | Logger.debug("foo") 144 | assert has_open(path()) 145 | 146 | new_path = path() <> ".1" 147 | {"", 0} = System.cmd("mv", [path(), new_path]) 148 | 149 | assert has_open(new_path) 150 | 151 | Logger.debug("bar") 152 | 153 | assert has_open(path()) 154 | refute has_open(new_path) 155 | end 156 | 157 | test "closes old log file after path has been changed" do 158 | Logger.debug("foo") 159 | assert has_open(path()) 160 | 161 | org_path = path() 162 | config(path: path() <> ".new") 163 | 164 | Logger.debug("bar") 165 | assert has_open(path()) 166 | refute has_open(org_path) 167 | end 168 | 169 | test "log file rotate" do 170 | config(format: "$message\n") 171 | config(rotate: %{max_bytes: 4, keep: 4}) 172 | 173 | Logger.debug("rotate1") 174 | Logger.debug("rotate2") 175 | Logger.debug("rotate3") 176 | Logger.debug("rotate4") 177 | Logger.debug("rotate5") 178 | Logger.debug("rotate6") 179 | 180 | p = path() 181 | 182 | assert File.read!("#{p}.4") == "rotate2\n" 183 | assert File.read!("#{p}.3") == "rotate3\n" 184 | assert File.read!("#{p}.2") == "rotate4\n" 185 | assert File.read!("#{p}.1") == "rotate5\n" 186 | assert File.read!(p) == "rotate6\n" 187 | 188 | config(rotate: nil) 189 | end 190 | 191 | test "log file not rotate" do 192 | config(format: "$message\n") 193 | config(rotate: %{max_bytes: 100, keep: 4}) 194 | 195 | words = ~w(rotate1 rotate2 rotate3 rotate4 rotate5 rotate6) 196 | words |> Enum.map(&Logger.debug(&1)) 197 | 198 | assert log() == Enum.join(words, "\n") <> "\n" 199 | 200 | config(rotate: nil) 201 | end 202 | 203 | test "Allow :all to metadata" do 204 | config(format: "$metadata") 205 | 206 | config(metadata: []) 207 | Logger.debug("metadata", metadata1: "foo", metadata2: "bar") 208 | assert log() == "" 209 | 210 | config(metadata: [:metadata3]) 211 | Logger.debug("metadata", metadata3: "foo", metadata4: "bar") 212 | assert log() == "metadata3=foo " 213 | 214 | config(metadata: :all) 215 | Logger.debug("metadata", metadata5: "foo", metadata6: "bar") 216 | 217 | # Match separately for metadata5/metadata6 to avoid depending on order 218 | contents = log() 219 | assert contents =~ "metadata5=foo" 220 | assert contents =~ "metadata6=bar" 221 | end 222 | 223 | defp has_open(path) do 224 | has_open(:os.type(), path) 225 | end 226 | 227 | defp has_open({:unix, _}, path) do 228 | case System.cmd("lsof", [path]) do 229 | {output, 0} -> 230 | output =~ System.pid() 231 | 232 | _ -> 233 | false 234 | end 235 | end 236 | 237 | defp has_open(_, _) do 238 | false 239 | end 240 | 241 | defp path do 242 | {:ok, path} = :gen_event.call(Logger, @backend, :path) 243 | path 244 | end 245 | 246 | defp log do 247 | File.read!(path()) 248 | end 249 | 250 | defp config(opts) do 251 | :ok = Logger.configure_backend(@backend, opts) 252 | end 253 | 254 | defp logfile(context, basedir) do 255 | logfile = 256 | context.test 257 | |> Atom.to_string() 258 | |> String.replace(" ", "_") 259 | 260 | Path.join(basedir, logfile) 261 | end 262 | end 263 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | :application.start(:logger) 2 | ExUnit.start() 3 | --------------------------------------------------------------------------------