diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec55cf..3798218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## v0.14.0 (2024-04-16) +* Support dynamic log level in `Uinta.Plug`. + * Option `:log` now accepts `{module, function, args}` tuple called with prepended `conn` to determine log level. + ## v0.13.0 (2024-01-09) ### Changed * Support not double encoding the payload. In order to do that, a new plugs option `format` was added. We are deprecating the `json` option instead though it is backward compatible for a little while diff --git a/lib/uinta/plug.ex b/lib/uinta/plug.ex index 237a6c9..13a6700 100644 --- a/lib/uinta/plug.ex +++ b/lib/uinta/plug.ex @@ -70,6 +70,7 @@ if Code.ensure_loaded?(Plug) do - `:log` - The log level at which this plug should log its request info. Default is `:info` + - Can be a `{module, function_name, args}` tuple where function is applied with `conn` prepended to args to determine log level. - `:format` - Output format, either :json, :string, or :map. Default is `:string` - `:json` - Whether or not plug should log in JSON format. Default is `false` (obsolete) - `:ignored_paths` - A list of paths that should not log requests. Default @@ -99,7 +100,7 @@ if Code.ensure_loaded?(Plug) do @type format :: :json | :map | :string @type graphql_info :: %{type: String.t(), operation: String.t(), variables: String.t() | nil} @type opts :: %{ - level: Logger.level(), + level: Logger.level() | {module(), atom(), list()}, format: format(), include_unnamed_queries: boolean(), include_variables: boolean(), @@ -148,7 +149,9 @@ if Code.ensure_loaded?(Plug) do defp log_request(conn, start, opts) do if should_log_request?(conn, opts) do - Logger.log(opts.level, fn -> + level = log_level(conn, opts) + + Logger.log(level, fn -> stop = System.monotonic_time() diff = System.convert_time_unit(stop - start, :native, :microsecond) @@ -160,6 +163,18 @@ if Code.ensure_loaded?(Plug) do end end + @spec log_level(Plug.Conn.t(), opts()) :: Logger.level() + defp log_level(conn, opts) + + defp log_level(_conn, %{level: level}) when is_atom(level) do + level + end + + defp log_level(conn, %{level: {module, function, args}}) + when is_atom(module) and is_atom(function) and is_list(args) do + apply(module, function, [conn | args]) + end + @spec info(Plug.Conn.t(), graphql_info(), integer(), opts()) :: map() defp info(conn, graphql_info, diff, opts) do info = %{ diff --git a/mix.exs b/mix.exs index 3fc5795..b161051 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule Uinta.MixProject do use Mix.Project @project_url "https://github.com/podium/uinta" - @version "0.13.0" + @version "0.14.0" def project do [ diff --git a/test/uinta/plug_test.exs b/test/uinta/plug_test.exs index 82118ac..72b452a 100644 --- a/test/uinta/plug_test.exs +++ b/test/uinta/plug_test.exs @@ -123,6 +123,37 @@ defmodule Uinta.PlugTest do end end + defmodule MyDynamicLevelPlug do + use Plug.Builder + + plug(Uinta.Plug, log: {__MODULE__, :level, [:some, :opts]}) + plug(:passthrough) + + def level(conn, :some, :opts) do + case conn.status do + info when info in 100..199 -> + :debug + + success when success in 200..299 -> + :info + + redirect when redirect in 300..399 -> + :notice + + client_error when client_error in 400..499 -> + :warning + + server_error when server_error in 500..599 -> + :error + end + end + + defp passthrough(conn, _opts) do + status = Map.fetch!(conn.private, :dynamic_status) + Plug.Conn.send_resp(conn, status, "Body shouldn't matter") + end + end + defmodule SampleSuccessPlug do use Plug.Builder @@ -342,6 +373,48 @@ defmodule Uinta.PlugTest do assert message =~ ~r"\[debug\] GET / - Sent 200 in [0-9]+[µm]s"u end + test "logs dynamic log low level" do + conn = + :get + |> conn("/") + |> put_private(:dynamic_status, 100) + + message = + capture_log(fn -> + MyDynamicLevelPlug.call(conn, []) + end) + + assert message =~ ~r"\[debug\] GET / - Sent 100 in [0-9]+[µm]s"u + end + + test "logs dynamic log mid level" do + conn = + :get + |> conn("/") + |> put_private(:dynamic_status, 307) + + message = + capture_log(fn -> + MyDynamicLevelPlug.call(conn, []) + end) + + assert message =~ ~r"\[notice\] GET / - Sent 307 in [0-9]+[µm]s"u + end + + test "logs dynamic log high level" do + conn = + :get + |> conn("/") + |> put_private(:dynamic_status, 502) + + message = + capture_log(fn -> + MyDynamicLevelPlug.call(conn, []) + end) + + assert message =~ ~r"\[error\] GET / - Sent 502 in [0-9]+[µm]s"u + end + test "ignores ignored_paths when a 200-level status is returned" do message = capture_log(fn ->