Current section

26 Versions

Jump to

Compare versions

12 files changed
+682 additions
-299 deletions
  @@ -5,105 +5,224 @@
5 5 [![Hex downloads](https://img.shields.io/hexpm/dt/logster.svg "Hex downloads")](https://hex.pm/packages/logster)
6 6 [![License](http://img.shields.io/:license-mit-blue.svg)](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…