-
Notifications
You must be signed in to change notification settings - Fork 363
Add metadata to logger #631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,7 @@ defmodule Tesla.Middleware.Logger.Formatter do | |
| # https://github.com/elixir-lang/elixir/blob/v1.6.4/lib/logger/lib/logger/formatter.ex | ||
|
|
||
| @default_format "$method $url -> $status ($time ms)" | ||
| @keys ~w(method url status time query) | ||
| @keys ~w(method url status time query request_body response_body) | ||
|
|
||
| @type format :: [atom | binary] | ||
|
|
||
|
|
@@ -43,18 +43,21 @@ defmodule Tesla.Middleware.Logger.Formatter do | |
| Enum.map(format, &output(&1, request, response, time)) | ||
| end | ||
|
|
||
| defp output(:query, env, _, _) do | ||
| def output(:query, env, _, _) do | ||
| encoding = Keyword.get(env.opts, :query_encoding, :www_form) | ||
|
|
||
| Tesla.encode_query(env.query, encoding) | ||
| end | ||
|
|
||
| defp output(:method, env, _, _), do: env.method |> to_string() |> String.upcase() | ||
| defp output(:url, env, _, _), do: env.url | ||
| defp output(:status, _, {:ok, env}, _), do: to_string(env.status) | ||
| defp output(:status, _, {:error, reason}, _), do: "error: " <> inspect(reason) | ||
| defp output(:time, _, _, time), do: :io_lib.format("~.3f", [time / 1000]) | ||
| defp output(binary, _, _, _), do: binary | ||
| def output(:method, env, _, _), do: env.method |> to_string() |> String.upcase() | ||
| def output(:url, env, _, _), do: env.url | ||
| def output(:status, _, {:ok, env}, _), do: to_string(env.status) | ||
| def output(:status, _, {:error, reason}, _), do: "error: " <> inspect(reason) | ||
| def output(:time, _, _, time), do: to_string(:io_lib.format("~.3f", [time / 1000])) | ||
| def output(:request_body, env, _, _), do: env.body | ||
| def output(:response_body, _, {:ok, env}, _), do: env.body | ||
| def output(:response_body, _, {:error, _}, _), do: nil | ||
| def output(binary, _, _, _), do: binary | ||
| end | ||
|
|
||
| defmodule Tesla.Middleware.Logger do | ||
|
|
@@ -82,6 +85,7 @@ defmodule Tesla.Middleware.Logger do | |
| - `:metadata` - metadata to pass to `Logger` | ||
| - `:debug` - use `Logger.debug/2` to log request/response details | ||
| - `:format` - custom string template or function for log message (see below) | ||
| - `:metadata` - configure logger metadata | ||
|
|
||
| ## Custom log format | ||
|
|
||
|
|
@@ -237,6 +241,27 @@ defmodule Tesla.Middleware.Logger do | |
| config :tesla, Tesla.Middleware.Logger, | ||
| filter_headers: ["authorization"] | ||
| ``` | ||
|
|
||
| ### Configure Logger metadata | ||
|
|
||
| Set `metadata: true` to include metadata in the log output. | ||
|
|
||
| ``` | ||
| plug Tesla.Middleware.Logger, metadata: true | ||
| ``` | ||
|
|
||
| Pass a list of atoms to `metadata` to include only specific metadata. | ||
|
|
||
| ``` | ||
| plug Tesla.Middleware.Logger, metadata: [:url, :status, :request_body] | ||
| ``` | ||
|
|
||
| Use `:conceal` request option to conceal sensitive requests. | ||
|
|
||
| ``` | ||
| Tesla.get(client, opts: [conceal: true]]) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overall, I like the idea, the only feedback is about documentation. We should document I like it! |
||
| ``` | ||
|
|
||
| """ | ||
|
|
||
| @behaviour Tesla.Middleware | ||
|
|
@@ -268,12 +293,17 @@ defmodule Tesla.Middleware.Logger do | |
| format = | ||
| if optional_runtime_format, do: Formatter.compile(optional_runtime_format), else: @format | ||
|
|
||
| metadata = Keyword.get(config, :metadata, []) | ||
| level = log_level(response, config) | ||
| Logger.log(level, fn -> Formatter.format(env, response, time, format) end, metadata) | ||
| metadata_opt = Keyword.get(config, :metadata, false) | ||
|
|
||
| Logger.log( | ||
| level, | ||
| fn -> Formatter.format(env, response, time, format) end, | ||
| metadata(env, response, time, metadata_opt) | ||
| ) | ||
|
|
||
| if Keyword.get(config, :debug, true) do | ||
| Logger.debug(fn -> debug(env, response, config) end, metadata) | ||
| Logger.debug(fn -> debug(env, response, config) end, metadata(env, response, time, metadata_opt)) | ||
| end | ||
|
|
||
| response | ||
|
|
@@ -343,6 +373,20 @@ defmodule Tesla.Middleware.Logger do | |
| end | ||
| end | ||
|
|
||
| @metadata_keys [:method, :url, :query, :status, :request_body, :response_body, :time] | ||
|
|
||
| defp metadata(req, res, time, keys) when is_list(keys) do | ||
| if req.opts[:conceal] do | ||
| [] | ||
| else | ||
| [tesla: Enum.into(keys, %{}, fn key -> {key, Formatter.output(key, req, res, time)} end)] | ||
| end | ||
|
Comment on lines
+376
to
+383
|
||
| end | ||
|
|
||
| defp metadata(req, res, time, true), do: metadata(req, res, time, @metadata_keys) | ||
|
|
||
| defp metadata(_req, _res, _time, false), do: [] | ||
|
Comment on lines
+376
to
+388
|
||
|
|
||
| @debug_no_query "(no query)" | ||
| @debug_no_headers "(no headers)" | ||
| @debug_no_body "(no body)" | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -223,19 +223,6 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe "with metadata" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setup do | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| middleware = [{Tesla.Middleware.Logger, metadata: [request_id: "abc123"]}] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| adapter = fn env -> {:ok, %{env | status: 200, body: "ok"}} end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| client = Tesla.client(middleware, adapter) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| %{client: client} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| test "metadata is passed to the main log call", %{client: client} do | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log = capture_log([metadata: [:request_id]], fn -> Tesla.get(client, "/ok") end) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert log =~ "[info] request_id=abc123" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe "with level" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| defmodule ClientWithLevel do | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -489,4 +476,92 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| assert Formatter.format(nil, nil, nil, {CompileMod, :format}) == "message" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe "Metadata" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| defmodule TestLoggerBackend do | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| def init(_mod), do: {:ok, []} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def handle_event({_level, _pid, {_mod, _msg, _time, meta}}, state) do | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| send(meta[:pid], {:metadata, meta}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {:ok, state} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def handle_event(_, state), do: {:ok, state} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| setup do | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {:ok, _} = Logger.add_backend(TestLoggerBackend) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| on_exit(fn -> Logger.remove_backend(TestLoggerBackend) end) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| adapter = fn env -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| case env.url do | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "/connection-error" -> {:error, :econnrefused} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "/server-error" -> {:ok, %{env | status: 500, body: "error"}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "/client-error" -> {:ok, %{env | status: 404, body: "error"}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "/redirect" -> {:ok, %{env | status: 301, body: "moved"}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "/ok" -> {:ok, %{env | status: 200, body: "ok"}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| [adapter: adapter] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| test "do not include metadata by default", %{adapter: adapter} do | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| middleware = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Tesla.Middleware.Logger | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| client = Tesla.client(middleware, adapter) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| capture_log(fn -> Tesla.get(client, "/ok") end) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_received {:metadata, meta} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| refute meta[:tesla] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| test "include all metadata when set to true", %{adapter: adapter} do | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| middleware = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {Tesla.Middleware.Logger, metadata: true} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| client = Tesla.client(middleware, adapter) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| capture_log(fn -> Tesla.post(client, "/ok", "reqdata") end) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_received {:metadata, meta} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert meta[:tesla] == %{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: "200", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| time: "0.000", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| url: "/ok", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| query: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| request_body: "reqdata", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| response_body: "ok" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+529
to
+537
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert meta[:tesla] == %{ | |
| status: "200", | |
| time: "0.000", | |
| url: "/ok", | |
| query: "", | |
| method: "POST", | |
| request_body: "reqdata", | |
| response_body: "ok" | |
| } | |
| tesla_meta = meta[:tesla] | |
| # Assert all metadata fields except time match exactly | |
| assert Map.delete(tesla_meta, :time) == %{ | |
| status: "200", | |
| url: "/ok", | |
| query: "", | |
| method: "POST", | |
| request_body: "reqdata", | |
| response_body: "ok" | |
| } | |
| # Assert time is present and is a non-negative float in string form | |
| assert is_binary(tesla_meta.time) | |
| assert {time_float, ""} = Float.parse(tesla_meta.time) | |
| assert time_float >= 0.0 |
Copilot
AI
Mar 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in test name: "reqeusts" should be "requests".
| test "do not include metadata for concealed reqeusts", %{adapter: adapter} do | |
| test "do not include metadata for concealed requests", %{adapter: adapter} do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:metadatais documented twice and the first description ("metadata to pass to Logger") no longer matches the implementation (it now treats:metadataasfalse | true | [atom]). This is also an API-breaking change for existing callers that passed keyword metadata (e.g.[request_id: ...]), so the option docs should be updated to reflect the supported shapes and/or a new option name should be introduced for the Tesla-specific metadata to preserve backward compatibility.