Current section
26 Versions
Jump to
Current section
26 Versions
Compare versions
12
files changed
+682
additions
-299
deletions
| @@ -5,105 +5,224 @@ | |
| 5 5 | [](https://hex.pm/packages/logster) |
| 6 6 | [](http://doge.mit-license.org) |
| 7 7 | |
| 8 | - An easy to parse, one line logger for Elixir Plug.Conn and Phoenix, inspired by [lograge](https://github.com/roidrage/lograge). |
| 8 | + > **Note**\ |
| 9 | + > This is the documentation for v2 of Logster. If you're looking for v1, see the [v1 branch](https://github.com/navinpeiris/logster/tree/v1.x). |
| 9 10 | |
| 10 | - With the default `Plug.Logger`, the log output for a request looks like: |
| 11 | + <!-- MDOC --> |
| 12 | + |
| 13 | + An easy-to-parse, single-line logger for Elixir Phoenix and Plug.Conn, inspired by [lograge](https://github.com/roidrage/lograge). Supports logfmt, JSON and custom output formatting. |
| 14 | + |
| 15 | + By default, Phoenix log output for a request looks similar to the following: |
| 11 16 | |
| 12 17 | ``` |
| 13 18 | [info] GET /articles/some-article |
| 19 | + [debug] Processing with HelloPhoenix.ArticleController.show/2 |
| 20 | + Parameters: %{"id" => "some-article"} |
| 21 | + Pipelines: [:browser] |
| 14 22 | [info] Sent 200 in 21ms |
| 15 23 | ``` |
| 16 24 | |
| 17 | - With Logster, we've got logging that's much easier to parse and search through, such as: |
| 25 | + This can be handy for development, but cumbersome for production. The log output is spread across multiple lines, making it difficult to parse and search. |
| 26 | + |
| 27 | + Logster aims to solve this problem by logging the request in a single line: |
| 18 28 | |
| 19 29 | ``` |
| 20 | - [info] method=GET path=/articles/some-article format=html controller=HelloPhoenix.ArticleController action=show params={"id":"some-article"} status=200 duration=0.402 state=set |
| 30 | + [info] state=sent method=GET path=/articles/some-article format=html controller=HelloPhoenix.ArticleController action=show params={"id":"some-article"} status=200 duration=0.402 |
| 21 31 | ``` |
| 22 32 | |
| 23 | - This becomes handy specially when integrating with log management services such as [Logentries](https://logentries.com/) or [Papertrail](https://papertrailapp.com/). |
| 33 | + This is especially handy when integrating with log management services such as [Logentries](https://logentries.com/) or [Papertrail](https://papertrailapp.com/). |
| 34 | + |
| 35 | + Alternatively, Logster can also output JSON formatted logs (see configuration section below): |
| 36 | + |
| 37 | + ``` |
| 38 | + [info] {"state":"sent","method":"GET","path":"/articles/some-article","format":"html","controller":"HelloPhoenix.ArticleController","action":"show","params":{"id":"some-article"},"status":200,"duration":0.402} |
| 39 | + ``` |
| 24 40 | |
| 25 41 | ## Installation |
| 26 42 | |
| 27 | - First, add Logster to your `mix.exs` dependencies: |
| 43 | + Add `:logster` to the list of dependencies in `mix.exs`: |
| 28 44 | |
| 29 45 | ```elixir |
| 30 46 | def deps do |
| 31 | - [{:logster, "~> 1.1"}] |
| 47 | + [{:logster, "~> 2.0.0-rc.1"}] |
| 32 48 | end |
| 33 49 | ``` |
| 34 50 | |
| 35 | - Then, update your dependencies: |
| 51 | + ## Upgrading |
| 36 52 | |
| 37 | - ``` |
| 38 | - $ mix deps.get |
| 39 | - ``` |
| 53 | + See [Upgrade Guide](UPGRADE_GUIDE.md) for more information. |
| 40 54 | |
| 41 55 | ## Usage |
| 42 56 | |
| 43 | - To use with a Phoenix application, replace `Plug.Logger` in the projects `endpoint.ex` file with `Logster.Plugs.Logger`: |
| 57 | + ### Using with Phoenix |
| 58 | + |
| 59 | + Attach the Logster Phoenix logger in the `start` function in your project's `application.ex` file: |
| 44 60 | |
| 45 61 | ```elixir |
| 46 | - # plug Plug.Logger |
| 47 | - plug Logster.Plugs.Logger |
| 62 | + # lib/my_app/application.ex |
| 63 | + def start(_type, _args) do |
| 64 | + children = [ |
| 65 | + # ... |
| 66 | + ] |
| 67 | + |
| 68 | + # |
| 69 | + # Add the line below: |
| 70 | + # |
| 71 | + :ok = Logster.attach_phoenix_logger() |
| 72 | + |
| 73 | + opts = [strategy: :one_for_one, name: MyApp.Supervisor] |
| 74 | + Supervisor.start_link(children, opts) |
| 75 | + end |
| 48 76 | ``` |
| 49 77 | |
| 50 | - To use it in as a plug, just add `plug Logster.Plugs.Logger` into the relevant module. |
| 78 | + Next, disable the default Phoenix logger by adding the following line to your `config.exs` file: |
| 79 | + |
| 80 | + ```elixir |
| 81 | + config :phoenix, :logger, false |
| 82 | + ``` |
| 83 | + |
| 84 | + ### Using with Plug |
| 85 | + |
| 86 | + Add `Logster.Plug` to your plug pipeline, or in the relevant module: |
| 87 | + |
| 88 | + ```elixir |
| 89 | + plug Logster.Plug |
| 90 | + ``` |
| 91 | + |
| 92 | + ### Using standalone logger |
| 93 | + |
| 94 | + Logster provides `debug`, `info`, `warning`, `error` etc convenience functions that mimic those provided by the elixir logger, which outputs messages in your chosen log format. |
| 95 | + |
| 96 | + For example: |
| 97 | + |
| 98 | + ```elixir |
| 99 | + Logger.info(service: :payments, event: :received, amount: 1000, customer: 123) |
| 100 | + ``` |
| 101 | + |
| 102 | + will output the following to the logs: |
| 103 | + |
| 104 | + ``` |
| 105 | + [info] service=payments event=received amount=1000 customer=123 |
| 106 | + ``` |
| 107 | + |
| 108 | + You can also provide a function to be called lazily, which will only be called if the log level is enabled: |
| 109 | + |
| 110 | + ```elixir |
| 111 | + Logger.debug(fn -> |
| 112 | + # some potentially expensive operation |
| 113 | + # won't be called if the log level is not enabled |
| 114 | + customer = get_customer_id() |
| 115 | + |
| 116 | + [service: :payments, event: :received, amount: 1000, customer: customer] |
| 117 | + end) |
| 118 | + ``` |
| 119 | + |
| 120 | + ## Configuration |
| 121 | + |
| 122 | + The following configuration options can be set through your `config.exs` file |
| 123 | + |
| 124 | + ### Formatter |
| 125 | + |
| 126 | + #### JSON formatter |
| 127 | + |
| 128 | + ```elixir |
| 129 | + config :logster, formatter: :json |
| 130 | + ``` |
| 131 | + |
| 132 | + _Caution:_ There is no guarantee that what reaches your console will be valid JSON. The Elixir `Logger` module has its own formatting which may be appended to your message. See the [Logger documentation](http://elixir-lang.org/docs/stable/logger/Logger.html) for more information. |
| 133 | + |
| 134 | + #### Custom formatter |
| 135 | + |
| 136 | + Provide a function that takes one argument, the parameters as input, and returns formatted output |
| 137 | + |
| 138 | + ```elixir |
| 139 | + config :logster, formatter: &MyCustomFormatter.format/1 |
| 140 | + ``` |
| 51 141 | |
| 52 142 | ### Filtering parameters |
| 53 143 | |
| 54 | - By default, Logster filters parameters named `password`, and replaces the content with `[FILTERED]`. |
| 144 | + By default, Logster filters parameters named `password`. |
| 55 145 | |
| 56 | - You can update the list of parameters that are filtered by adding the following to your configuration file: |
| 146 | + To change the filtered parameters: |
| 57 147 | |
| 58 148 | ```elixir |
| 59 | - config :logster, :filter_parameters, ["password", "secret", "token"] |
| 149 | + config :logster, filter_parameters: ["password", "secret", "token"] |
| 60 150 | ``` |
| 61 151 | |
| 62 | - ### HTTP headers support |
| 152 | + ### Logging HTTP request headers |
| 63 153 | |
| 64 | - By default, Logster won't parse and log HTTP headers. |
| 65 | - |
| 66 | - But you can update the list of headers that should be parsed and logged. The logged headers will be added under `headers`. Both plain text and JSON formatters are supported. |
| 154 | + By default, Logster won't log any request headers. To log specific headers, you can use the `:headers` option: |
| 67 155 | |
| 68 156 | ```elixir |
| 69 | - config :logster, :allowed_headers, ["my-header-one", "my-header-two"] |
| 157 | + config :logster, headers: ["my-header-one", "my-header-two"] |
| 70 158 | ``` |
| 71 159 | |
| 72 160 | ### Changing the log level for a specific controller/action |
| 73 161 | |
| 74 | - To change the Logster log level for a specific controller and/or action, you use the `Logster.Plugs.ChangeLogLevel` plug. |
| 162 | + #### Through Logster.ChangeLogLevel plug |
| 163 | + |
| 164 | + To change the Logster log level for a specific controller and/or action, you use the `Logster.ChangeLogLevel` plug. |
| 75 165 | |
| 76 166 | For example, to change the logging of all requests in a controller to `debug`, add the following to that controller: |
| 77 167 | |
| 78 168 | ```elixir |
| 79 | - plug Logster.Plugs.ChangeLogLevel, to: :debug |
| 169 | + plug Logster.ChangeLogLevel, to: :debug |
| 80 170 | ``` |
| 81 171 | |
| 82 172 | And to change it only for `index` and `show` actions: |
| 83 173 | |
| 84 174 | ```elixir |
| 85 | - plug Logster.Plugs.ChangeLogLevel, [to: :debug] when action in [:index, :show] |
| 175 | + plug Logster.ChangeLogLevel, [to: :debug] when action in [:index, :show] |
| 86 176 | ``` |
| 87 177 | |
| 88 178 | This is specially useful for cases such as when you want to lower the log level for a healthcheck endpoint that gets hit every few seconds. |
| 89 179 | |
| 90 | - ### Changing the formatter |
| 180 | + #### Through endpoint configuration |
| 91 181 | |
| 92 | - Logster allows you to use a different formatter to get your log lines looking just how you want. It comes with two built-in formatters: `Logster.StringFormatter` and `Logster.JSONFormatter` |
| 182 | + You can set the `Plug.Telemetry` `:log` option to a tuple, `{Mod, Fun, Args}`. `The Plug.Conn.t()` for the request will be prepended to the provided list of arguments. |
| 93 183 | |
| 94 | - To use `Logster.JSONFormatter`, supply the `formatter` option when you use the `Logster.Plugs.Logger` plug: |
| 184 | + When invoked, your function must return a `Logger.level()` or `false` to disable logging for the request. |
| 95 185 | |
| 96 186 | ```elixir |
| 97 | - plug Logster.Plugs.Logger, formatter: Logster.JSONFormatter |
| 187 | + # lib/my_app_web/endpoint.ex |
| 188 | + plug Plug.Telemetry, |
| 189 | + event_prefix: [:phoenix, :endpoint], |
| 190 | + log: {__MODULE__, :log_level, []} |
| 191 | + |
| 192 | + # Disables logging for routes like /status/* |
| 193 | + def log_level(%{status: status}) when status >= 500, do: :error |
| 194 | + def log_level(%{status: status}) when status >= 400, do: :warning |
| 195 | + def log_level(%{path_info: ["status" | _]}), do: false |
| 196 | + def log_level(_), do: :info |
| 98 197 | ``` |
| 99 198 | |
| 100 | - That means your log messages will be formatted thusly: |
| 199 | + ### Renaming default fields |
| 101 200 | |
| 102 | - ``` |
| 103 | - {"status":200,"state":"set","path":"hello","params":{},"method":"GET","format":"json","duration":20.647,"controller":"App.HelloController","action":"show" |
| 201 | + You can rename the default keys passing a map like `%{key: :new_key}`: |
| 202 | + |
| 203 | + ```elixir |
| 204 | + config :logster, renames: [duration: :response_time, params: :parameters] |
| 104 205 | ``` |
| 105 206 | |
| 106 | - _Caution:_ There is no guarantee that what reaches your console will be valid JSON. The Elixir `Logger` module has its own formatting which may be appended to your message. See the [Logger documentation](http://elixir-lang.org/docs/stable/logger/Logger.html) for more information. |
| 207 | + Example output: |
| 208 | + |
| 209 | + ``` |
| 210 | + [info] method=GET path=/articles/some-article format=html controller=HelloPhoenix.ArticleController action=show parameters={"id":"some-article"} status=200 response_time=0.402 state=set |
| 211 | + ``` |
| 212 | + |
| 213 | + ### Excluding fields |
| 214 | + |
| 215 | + You can exclude fields with `:excludes`: |
| 216 | + |
| 217 | + ```elixir |
| 218 | + config :logster, excludes: [:params, :status, :state] |
| 219 | + ``` |
| 220 | + |
| 221 | + Example output: |
| 222 | + |
| 223 | + ``` |
| 224 | + [info] method=GET path=/articles/some-article format=html controller=HelloPhoenix.ArticleController action=show duration=0.402 |
| 225 | + ``` |
| 107 226 | |
| 108 227 | ### Metadata |
| 109 228 | |
| @@ -119,7 +238,7 @@ Logger.metadata(%{user_id: "123", foo: "bar"}) | |
| 119 238 | config :logger, :console, metadata: [:user_id, :foo] |
| 120 239 | ``` |
| 121 240 | |
| 122 | - The easiest way to do this app wide is to introduce a new plug which you can include in your phoenix router pipeline. |
| 241 | + The easiest way to do this app wide is to introduce a new plug which you can include in your Phoenix router pipeline. |
| 123 242 | |
| 124 243 | For example: |
| 125 244 | |
| @@ -154,37 +273,7 @@ pipeline :browser do | |
| 154 273 | end |
| 155 274 | ``` |
| 156 275 | |
| 157 | - ### Renaming default fields |
| 158 | - |
| 159 | - You can rename the default keys passing a map like `%{key: :new_key}`: |
| 160 | - |
| 161 | - ```elixir |
| 162 | - plug Logster.Plugs.Logger, renames: %{duration: :response_time, params: :parameters} |
| 163 | - ``` |
| 164 | - |
| 165 | - It will log the following: |
| 166 | - |
| 167 | - ``` |
| 168 | - [info] method=GET path=/articles/some-article format=html controller=HelloPhoenix.ArticleController action=show parameters={"id":"some-article"} status=200 response_time=0.402 state=set |
| 169 | - ``` |
| 170 | - |
| 171 | - ### Excluding fields |
| 172 | - |
| 173 | - You can exclude fields with `:excludes`: |
| 174 | - |
| 175 | - ```elixir |
| 176 | - plug Logster.Plugs.Logger, excludes: [:params, :status, :state] |
| 177 | - ``` |
| 178 | - |
| 179 | - It will log the following: |
| 180 | - |
| 181 | - ``` |
| 182 | - [info] method=GET path=/articles/some-article format=html controller=HelloPhoenix.ArticleController action=show duration=0.402 |
| 183 | - ``` |
| 184 | - |
| 185 | - #### Writing your own formatter |
| 186 | - |
| 187 | - To write your own formatter, all that is required is a module which defines a `format/1` function, which accepts a keyword list and returns a string. |
| 276 | + <!-- MDOC --> |
| 188 277 | |
| 189 278 | ## Development |
| @@ -1,25 +1,25 @@ | |
| 1 | - {<<"app">>,<<"logster">>}. |
| 2 | - {<<"build_tools">>,[<<"mix">>]}. |
| 3 | - {<<"description">>, |
| 4 | - <<"Easily parsable single-line plain text and JSON logger for Plug and Phoenix applications">>}. |
| 5 | - {<<"elixir">>,<<"~> 1.9">>}. |
| 6 | - {<<"files">>, |
| 7 | - [<<"lib">>,<<"lib/logster">>,<<"lib/logster/json_formatter.ex">>, |
| 8 | - <<"lib/logster/plugs">>,<<"lib/logster/plugs/change_log_level.ex">>, |
| 9 | - <<"lib/logster/plugs/logger.ex">>,<<"lib/logster/string_formatter.ex">>, |
| 10 | - <<"mix.exs">>,<<"README.md">>,<<"LICENSE.md">>]}. |
| 11 | - {<<"licenses">>,[<<"MIT">>]}. |
| 12 1 | {<<"links">>,[{<<"GitHub">>,<<"https://github.com/navinpeiris/logster">>}]}. |
| 13 2 | {<<"name">>,<<"logster">>}. |
| 3 | + {<<"version">>,<<"2.0.0-rc.1">>}. |
| 4 | + {<<"description">>, |
| 5 | + <<"Easily parsable single-line plain text and JSON logger for Plug and Phoenix applications">>}. |
| 6 | + {<<"elixir">>,<<"~> 1.15">>}. |
| 7 | + {<<"app">>,<<"logster">>}. |
| 8 | + {<<"files">>, |
| 9 | + [<<"lib">>,<<"lib/logster.ex">>,<<"lib/logster">>, |
| 10 | + <<"lib/logster/change_log_level.ex">>,<<"lib/logster/formatters">>, |
| 11 | + <<"lib/logster/formatters/string.ex">>,<<"lib/logster/formatters/json.ex">>, |
| 12 | + <<"lib/logster/plug.ex">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE.md">>]}. |
| 13 | + {<<"licenses">>,[<<"MIT">>]}. |
| 14 14 | {<<"requirements">>, |
| 15 | - [[{<<"app">>,<<"plug">>}, |
| 16 | - {<<"name">>,<<"plug">>}, |
| 15 | + [[{<<"name">>,<<"plug">>}, |
| 16 | + {<<"app">>,<<"plug">>}, |
| 17 17 | {<<"optional">>,false}, |
| 18 | - {<<"repository">>,<<"hexpm">>}, |
| 19 | - {<<"requirement">>,<<"~> 1.0">>}], |
| 20 | - [{<<"app">>,<<"jason">>}, |
| 21 | - {<<"name">>,<<"jason">>}, |
| 18 | + {<<"requirement">>,<<"~> 1.0">>}, |
| 19 | + {<<"repository">>,<<"hexpm">>}], |
| 20 | + [{<<"name">>,<<"jason">>}, |
| 21 | + {<<"app">>,<<"jason">>}, |
| 22 22 | {<<"optional">>,false}, |
| 23 | - {<<"repository">>,<<"hexpm">>}, |
| 24 | - {<<"requirement">>,<<"~> 1.1">>}]]}. |
| 25 | - {<<"version">>,<<"1.1.1">>}. |
| 23 | + {<<"requirement">>,<<"~> 1.1">>}, |
| 24 | + {<<"repository">>,<<"hexpm">>}]]}. |
| 25 | + {<<"build_tools">>,[<<"mix">>]}. |
| @@ -0,0 +1,393 @@ | |
| 1 | + defmodule Logster do |
| 2 | + @external_resource readme = Path.join([__DIR__, "../README.md"]) |
| 3 | + |
| 4 | + @moduledoc readme |
| 5 | + |> File.read!() |
| 6 | + |> String.split("<!-- MDOC -->") |
| 7 | + |> Enum.fetch!(1) |
| 8 | + |
| 9 | + require Logger |
| 10 | + |
| 11 | + @default_filter_parameters ~w(password) |
| 12 | + |
| 13 | + # Taken from `Logger` module |
| 14 | + @levels [:emergency, :alert, :critical, :error, :warning, :notice, :info, :debug] |
| 15 | + |
| 16 | + @phoenix_handler_id {__MODULE__, :phoenix} |
| 17 | + |
| 18 | + @doc """ |
| 19 | + Attaches a telemetry handler to the `:phoenix` event stream for logging. |
| 20 | + |
| 21 | + Returns `:ok`. |
| 22 | + """ |
| 23 | + @spec attach_phoenix_logger :: :ok |
| 24 | + def attach_phoenix_logger do |
| 25 | + events = [ |
| 26 | + [:phoenix, :endpoint, :stop], |
| 27 | + [:phoenix, :socket_connected], |
| 28 | + [:phoenix, :channel_joined], |
| 29 | + [:phoenix, :channel_handled_in] |
| 30 | + ] |
| 31 | + |
| 32 | + :telemetry.attach_many( |
| 33 | + @phoenix_handler_id, |
| 34 | + events, |
| 35 | + &__MODULE__.handle_phoenix_event/4, |
| 36 | + :ok |
| 37 | + ) |
| 38 | + end |
| 39 | + |
| 40 | + @doc """ |
| 41 | + Detaches logster's telemetry handler from the `:phoenix` event stream. |
| 42 | + |
| 43 | + Returns `:ok`. |
| 44 | + """ |
| 45 | + @spec detach_phoenix_logger :: :ok |
| 46 | + def detach_phoenix_logger, do: :telemetry.detach(@phoenix_handler_id) |
| 47 | + |
| 48 | + def levels, do: @levels |
| 49 | + |
| 50 | + for level <- @levels do |
| 51 | + @doc """ |
| 52 | + Logs a #{level} message. See `Logster.log/3` for more information. |
| 53 | + |
| 54 | + Returns `:ok`. |
| 55 | + """ |
| 56 | + @spec unquote(level)(message :: Logger.message(), metadata :: Logger.metadata()) :: :ok |
| 57 | + def unquote(level)(fields_or_message_or_func, metadata \\ []) |
| 58 | + |
| 59 | + def unquote(level)(func, metadata) when is_function(func), |
| 60 | + do: Logger.unquote(level)(fn -> formatter().format(func.()) end, metadata) |
| 61 | + |
| 62 | + def unquote(level)(fields_or_message, metadata), |
| 63 | + do: Logger.unquote(level)(fn -> formatter().format(fields_or_message) end, metadata) |
| 64 | + end |
| 65 | + |
| 66 | + @doc """ |
| 67 | + Logs a message with the given `level`. |
| 68 | + |
| 69 | + If given an enumerable, the enumerable will be formatted using the configured formatter. |
| 70 | + |
| 71 | + Returns `:ok`. |
| 72 | + |
| 73 | + Using `debug/2`, `info/2`, `notice/2`, `warning/2`, `error/2`, `critical/2`, `alert/2`, |
| 74 | + and `emergency/2` are preferred over this function as they can automatically eliminate |
| 75 | + the call to `Logger` altogether at compile time if desired |
| 76 | + (see the documentation for the `Logger` module). |
| 77 | + |
| 78 | + ## Example |
| 79 | + |
| 80 | + ``` |
| 81 | + Logster.log(:info, service: "payment-processor", event: "start-processing", customer: "1234") |
| 82 | + ``` |
| 83 | + |
| 84 | + will produce the following log entry when using the `logfmt` formatter: |
| 85 | + |
| 86 | + ``` |
| 87 | + 16:54:29.919 [info] service=payment-processor event=start-processing customer=1234 |
| 88 | + ``` |
| 89 | + """ |
| 90 | + @spec log( |
| 91 | + level :: Logger.level(), |
| 92 | + message :: Logger.message(), |
| 93 | + metadata :: Logger.metadata() |
| 94 | + ) :: :ok |
| 95 | + def log(level, fields_or_message_or_func, metadata \\ []) |
| 96 | + |
| 97 | + def log(level, func, metadata) when is_function(func), |
| 98 | + do: Logger.log(level, fn -> formatter().format(func.()) end, metadata) |
| 99 | + |
| 100 | + def log(level, fields_or_message, metadata) do |
| 101 | + Logger.log( |
| 102 | + level, |
| 103 | + fn -> formatter().format(fields_or_message) end, |
| 104 | + metadata |
| 105 | + ) |
| 106 | + end |
| 107 | + |
| 108 | + @doc """ |
| 109 | + Logs details about the given `conn` at the given `level`. |
| 110 | + |
| 111 | + See the module documentation for more information on configuration options. |
| 112 | + |
| 113 | + Returns `:ok`. |
| 114 | + """ |
| 115 | + @spec log_conn( |
| 116 | + level :: Logger.level(), |
| 117 | + conn :: Plug.Conn.t(), |
| 118 | + duration_us :: integer(), |
| 119 | + metadata :: Logger.metadata() |
| 120 | + ) :: :ok |
| 121 | + def log_conn(level, conn, duration_us, metadata \\ []) do |
| 122 | + log( |
| 123 | + level, |
| 124 | + fn -> conn |> get_conn_fields(duration: format_duration(duration_us)) end, |
| 125 | + metadata |
| 126 | + ) |
| 127 | + end |
| 128 | + |
| 129 | + defp formatter(formatter \\ Application.get_env(:logster, :formatter, :string)) |
| 130 | + defp formatter(:string), do: Logster.Formatters.String |
| 131 | + defp formatter(:json), do: Logster.Formatters.JSON |
| 132 | + defp formatter(other), do: other |
| 133 | + |
| 134 | + @doc false |
| 135 | + def log_level(_, %{private: %{logster_log_level: level}}), do: level |
| 136 | + |
| 137 | + def log_level(nil, %{status: status}) when is_integer(status) and status >= 500, do: :error |
| 138 | + def log_level(nil, %{status: status}) when is_integer(status) and status >= 400, do: :warning |
| 139 | + def log_level(nil, _conn), do: :info |
| 140 | + |
| 141 | + def log_level(level, _conn) when is_atom(level), do: level |
| 142 | + |
| 143 | + def log_level({mod, fun, args}, conn) when is_atom(mod) and is_atom(fun) and is_list(args) do |
| 144 | + apply(mod, fun, [conn | args]) |
| 145 | + end |
| 146 | + |
| 147 | + @doc false |
| 148 | + def handle_phoenix_event( |
| 149 | + [:phoenix, :endpoint, :stop], |
| 150 | + %{duration: duration}, |
| 151 | + %{conn: conn} = metadata, |
| 152 | + _ |
| 153 | + ) do |
| 154 | + case log_level(metadata[:options][:log], conn) do |
| 155 | + false -> :ok |
| 156 | + level -> log_conn(level, conn, duration) |
| 157 | + end |
| 158 | + end |
| 159 | + |
| 160 | + @doc false |
| 161 | + def handle_phoenix_event([:phoenix, :socket_connected], _, %{log: false}, _), do: :ok |
| 162 | + |
| 163 | + @doc false |
| 164 | + def handle_phoenix_event( |
| 165 | + [:phoenix, :socket_connected], |
| 166 | + %{duration: duration}, |
| 167 | + %{log: level} = meta, |
| 168 | + _ |
| 169 | + ) do |
| 170 | + log(level, fn -> |
| 171 | + %{ |
| 172 | + transport: transport, |
| 173 | + params: params, |
| 174 | + user_socket: user_socket, |
| 175 | + result: result, |
| 176 | + serializer: serializer |
| 177 | + } = meta |
| 178 | + |
| 179 | + [ |
| 180 | + action: :connect, |
| 181 | + state: result, |
| 182 | + socket: inspect(user_socket), |
| 183 | + duration: format_duration(duration), |
| 184 | + transport: Atom.to_string(transport), |
| 185 | + serializer: inspect(serializer) |
| 186 | + ] |
| 187 | + |> append_params(params) |
| 188 | + end) |
| 189 | + end |
| 190 | + |
| 191 | + @doc false |
| 192 | + def handle_phoenix_event( |
| 193 | + [:phoenix, :channel_joined], |
| 194 | + %{duration: duration}, |
| 195 | + %{socket: socket} = metadata, |
| 196 | + _ |
| 197 | + ) do |
| 198 | + channel_log(:log_join, socket, fn -> |
| 199 | + %{result: result, params: params} = metadata |
| 200 | + |
| 201 | + [ |
| 202 | + action: :join, |
| 203 | + state: result, |
| 204 | + topic: socket.topic, |
| 205 | + duration: format_duration(duration) |
| 206 | + ] |
| 207 | + |> append_params(params) |
| 208 | + end) |
| 209 | + end |
| 210 | + |
| 211 | + @doc false |
| 212 | + def handle_phoenix_event( |
| 213 | + [:phoenix, :channel_handled_in], |
| 214 | + %{duration: duration}, |
| 215 | + %{socket: socket} = metadata, |
| 216 | + _ |
| 217 | + ) do |
| 218 | + channel_log(:log_handle_in, socket, fn -> |
| 219 | + %{event: event, params: params} = metadata |
| 220 | + |
| 221 | + [ |
| 222 | + action: :handled, |
| 223 | + event: event, |
| 224 | + topic: socket.topic, |
| 225 | + channel: inspect(socket.channel), |
| 226 | + duration: format_duration(duration) |
| 227 | + ] |
| 228 | + |> append_params(params) |
| 229 | + end) |
| 230 | + end |
| 231 | + |
| 232 | + defp channel_log(_log_option, %{topic: "phoenix" <> _}, _fun), do: :ok |
| 233 | + |
| 234 | + defp channel_log(log_option, %{private: private}, fun) do |
| 235 | + if level = Map.get(private, log_option) do |
| 236 | + log(level, fun) |
| 237 | + end |
| 238 | + end |
| 239 | + |
| 240 | + @spec get_conn_fields(Plug.Conn.t(), keyword) :: list |
| 241 | + @doc false |
| 242 | + def get_conn_fields(%Plug.Conn{} = conn, extra_fields \\ []) do |
| 243 | + # We use `Keyword.put` to add items to the list, which prepends items to the list, and so we |
| 244 | + # add items in reverse order of how we want them to appear in the log message. |
| 245 | + extra_fields |
| 246 | + |> maybe_put_headers(conn) |
| 247 | + |> Keyword.put(:status, conn.status) |
| 248 | + |> put_params(conn) |
| 249 | + |> maybe_put_phoenix_info(conn) |
| 250 | + |> Keyword.put(:path, conn.request_path) |
| 251 | + |> Keyword.put(:method, conn.method) |
| 252 | + |> Keyword.put(:state, format_state(conn.state)) |
| 253 | + |> maybe_remove_excluded_fields() |
| 254 | + |> maybe_rename_fields() |
| 255 | + end |
| 256 | + |
| 257 | + defp format_state(:set_chunked), do: "chunked" |
| 258 | + defp format_state(_), do: "sent" |
| 259 | + |
| 260 | + defp maybe_put_phoenix_info(fields, %Plug.Conn{ |
| 261 | + private: %{phoenix_controller: controller, phoenix_action: action} |
| 262 | + }) do |
| 263 | + fields |
| 264 | + |> Keyword.put(:action, Atom.to_string(action)) |
| 265 | + |> Keyword.put(:controller, inspect(controller)) |
| 266 | + end |
| 267 | + |
| 268 | + defp maybe_put_phoenix_info(fields, _), do: fields |
| 269 | + |
| 270 | + defp put_params(fields, %Plug.Conn{params: %Plug.Conn.Unfetched{}}), |
| 271 | + do: fields |> Keyword.put(:params, "[UNFETCHED]") |
| 272 | + |
| 273 | + defp put_params(fields, %Plug.Conn{params: params}), do: put_params(fields, params) |
| 274 | + |
| 275 | + defp put_params(fields, params) do |
| 276 | + params = |
| 277 | + params |
| 278 | + |> filter_values() |
| 279 | + |> format_values() |
| 280 | + |
| 281 | + fields |> Keyword.put(:params, params) |
| 282 | + end |
| 283 | + |
| 284 | + # convenience method to put the params at the end of the given list |
| 285 | + defp append_params(fields, params), do: fields ++ put_params([], params) |
| 286 | + |
| 287 | + defp maybe_put_headers(fields, conn), |
| 288 | + do: do_maybe_put_headers(fields, conn, Application.get_env(:logster, :headers, [])) |
| 289 | + |
| 290 | + defp do_maybe_put_headers(fields, _conn, []), do: fields |
| 291 | + |
| 292 | + defp do_maybe_put_headers(fields, conn, loggable_headers) do |
| 293 | + headers = |
| 294 | + conn.req_headers |
| 295 | + |> Enum.filter(fn {k, _} -> Enum.member?(loggable_headers, k) end) |
| 296 | + |> Enum.into(%{}, fn {k, v} -> {k, v} end) |
| 297 | + |
| 298 | + fields |> Keyword.put(:headers, headers) |
| 299 | + end |
| 300 | + |
| 301 | + defp maybe_remove_excluded_fields(fields), |
| 302 | + do: fields |> Keyword.drop(Application.get_env(:logster, :excludes, [])) |
| 303 | + |
| 304 | + defp maybe_rename_fields(params, renames \\ Application.get_env(:logster, :renames, [])) |
| 305 | + |
| 306 | + defp maybe_rename_fields(fields, []), do: fields |
| 307 | + |
| 308 | + defp maybe_rename_fields(fields, renames) do |
| 309 | + renames_map = renames |> Enum.into(%{}) |
| 310 | + |
| 311 | + fields |
| 312 | + |> Enum.map(fn {key, value} -> |
| 313 | + if new_key = Map.get(renames_map, key) do |
| 314 | + {new_key, value} |
| 315 | + else |
| 316 | + {key, value} |
| 317 | + end |
| 318 | + end) |
| 319 | + end |
| 320 | + |
| 321 | + defp filter_values( |
| 322 | + params, |
| 323 | + filter_config \\ Application.get_env( |
| 324 | + :logster, |
| 325 | + :filter_parameters, |
| 326 | + @default_filter_parameters |
| 327 | + ) |
| 328 | + ) |
| 329 | + |
| 330 | + defp filter_values(params, {:discard, discard_params}), |
| 331 | + do: discard_values(params, discard_params) |
| 332 | + |
| 333 | + defp filter_values(params, {:keep, keep_params}), do: keep_values(params, keep_params) |
| 334 | + defp filter_values(params, filtered_params), do: discard_values(params, filtered_params) |
| 335 | + |
| 336 | + defp discard_values(%{__struct__: mod} = struct, filter_config) when is_atom(mod) do |
| 337 | + struct |
| 338 | + |> Map.from_struct() |
| 339 | + |> Enum.map(fn {k, v} -> {Atom.to_string(k), v} end) |
| 340 | + |> Enum.into(%{}) |
| 341 | + |> discard_values(filter_config) |
| 342 | + end |
| 343 | + |
| 344 | + defp discard_values(%{} = map, discard_params) do |
| 345 | + Enum.into(map, %{}, fn {k, v} -> |
| 346 | + if is_binary(k) and String.contains?(k, discard_params) do |
| 347 | + {k, "[FILTERED]"} |
| 348 | + else |
| 349 | + {k, discard_values(v, discard_params)} |
| 350 | + end |
| 351 | + end) |
| 352 | + end |
| 353 | + |
| 354 | + defp discard_values([_ | _] = list, discard_params), |
| 355 | + do: Enum.map(list, &discard_values(&1, discard_params)) |
| 356 | + |
| 357 | + defp discard_values(other, _discard_params), do: other |
| 358 | + |
| 359 | + defp keep_values(%{__struct__: mod}, _keep_params) when is_atom(mod), do: "[FILTERED]" |
| 360 | + |
| 361 | + defp keep_values(%{} = map, keep_params) do |
| 362 | + Enum.into(map, %{}, fn {k, v} -> |
| 363 | + if is_binary(k) and k in keep_params do |
| 364 | + {k, discard_values(v, [])} |
| 365 | + else |
| 366 | + {k, keep_values(v, keep_params)} |
| 367 | + end |
| 368 | + end) |
| 369 | + end |
| 370 | + |
| 371 | + defp keep_values([_ | _] = list, keep_params) do |
| 372 | + Enum.map(list, &keep_values(&1, keep_params)) |
| 373 | + end |
| 374 | + |
| 375 | + defp keep_values(_other, _keep_params), do: "[FILTERED]" |
| 376 | + |
| 377 | + defp format_values(params), do: params |> Enum.into(%{}, &format_value/1) |
| 378 | + |
| 379 | + defp format_value({key, value}) when is_binary(value) do |
| 380 | + if String.valid?(value) do |
| 381 | + {key, value} |
| 382 | + else |
| 383 | + {key, URI.encode(value)} |
| 384 | + end |
| 385 | + end |
| 386 | + |
| 387 | + defp format_value(val), do: val |
| 388 | + |
| 389 | + defp format_duration(duration) do |
| 390 | + microseconds = duration |> System.convert_time_unit(:native, :microsecond) |
| 391 | + microseconds / 1000 |
| 392 | + end |
| 393 | + end |
| @@ -0,0 +1,19 @@ | |
| 1 | + defmodule Logster.ChangeLogLevel do |
| 2 | + @moduledoc """ |
| 3 | + A plug for changing the log level used by Logster. Useful for increasing/decreasing the log level on a per controller or action basis |
| 4 | + |
| 5 | + To change the log level for a specific controller, add the following in the controller |
| 6 | + |
| 7 | + plug Logster.Plugs.ChangeLogLevel, to: :debug |
| 8 | + |
| 9 | + To specify it only for a specific action, add the following: |
| 10 | + |
| 11 | + plug Logster.Plugs.ChangeLogLevel, to: :debug when action in [:index, :show] |
| 12 | + """ |
| 13 | + |
| 14 | + import Plug.Conn |
| 15 | + |
| 16 | + def init(opts), do: Keyword.get(opts, :to, :info) |
| 17 | + |
| 18 | + def call(conn, log_level), do: conn |> put_private(:logster_log_level, log_level) |
| 19 | + end |
| @@ -0,0 +1,15 @@ | |
| 1 | + defmodule Logster.Formatters.JSON do |
| 2 | + def format(data) when is_map(data) do |
| 3 | + case data |> Jason.encode_to_iodata() do |
| 4 | + {:ok, json} -> json |
| 5 | + {:error, _} -> %{msg: inspect(data)} |> format() |
| 6 | + end |
| 7 | + end |
| 8 | + |
| 9 | + def format([]), do: "" |
| 10 | + def format([{_, _} | _] = data), do: data |> Enum.into(%{}) |> format() |
| 11 | + |
| 12 | + def format(data) when is_binary(data) or is_list(data), do: %{msg: data} |> format() |
| 13 | + |
| 14 | + def format(data), do: %{msg: inspect(data)} |> format() |
| 15 | + end |
Loading more files…