• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Nebo15 / logger_json / 2f0e88b3be770b43bc8c9fd1782ede6118395b90

03 Apr 2026 09:30PM UTC coverage: 99.737% (-0.3%) from 100.0%
2f0e88b3be770b43bc8c9fd1782ede6118395b90

push

github

AndrewDryga
Support special metadata keys :module and :function in BasicFormatter

6 of 7 new or added lines in 2 files covered. (85.71%)

379 of 380 relevant lines covered (99.74%)

2276.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.97
/lib/logger_json/formatters/basic.ex
1
defmodule LoggerJSON.Formatters.Basic do
2
  @moduledoc """
3
  Custom Erlang's [`:logger` formatter](https://www.erlang.org/doc/apps/kernel/logger_chapter.html#formatters) which
4
  writes logs in a JSON format.
5

6
  For list of options see "Shared options" in `LoggerJSON`.
7

8
  ## Examples
9

10
      %{
11
        "message" => "Hello",
12
        "metadata" => %{"domain" => ["elixir"]},
13
        "severity" => "notice",
14
        "time" => "2024-04-11T21:31:01.403Z"
15
      }
16
  """
17
  import LoggerJSON.Formatter.{MapBuilder, DateTime, Message, Metadata, RedactorEncoder}
18
  require LoggerJSON.Formatter, as: Formatter
19

20
  @behaviour Formatter
21

22
  @encoder Formatter.encoder()
23

24
  @processed_metadata_keys ~w[otel_span_id span_id
25
                              otel_trace_id trace_id
26
                              conn]a
27

28
  @impl Formatter
29
  def new(opts \\ []) do
30
    {__MODULE__, config(opts)}
31
  end
32

33
  defp config(%{} = map), do: map
328✔
34

35
  defp config(opts) do
36
    opts = Keyword.new(opts)
33✔
37
    encoder_opts = Keyword.get_lazy(opts, :encoder_opts, &Formatter.default_encoder_opts/0)
33✔
38
    metadata_keys_or_selector = Keyword.get(opts, :metadata, [])
33✔
39
    metadata_selector = update_metadata_selector(metadata_keys_or_selector, @processed_metadata_keys)
33✔
40
    redactors = Keyword.get(opts, :redactors, [])
33✔
41
    %{encoder_opts: encoder_opts, metadata: metadata_selector, redactors: redactors}
33✔
42
  end
43

44
  @impl Formatter
45
  def format(%{level: level, meta: meta, msg: msg}, config_or_opts) do
46
    %{
47
      encoder_opts: encoder_opts,
48
      metadata: metadata_selector,
49
      redactors: redactors
50
    } = config(config_or_opts)
329✔
51

52
    message =
329✔
53
      format_message(msg, meta, %{
54
        binary: &format_binary_message/1,
55
        structured: &format_structured_message/1,
56
        crash: &format_crash_reason(&1, &2, meta)
1✔
57
      })
58

59
    metadata =
329✔
60
      meta
61
      |> take_metadata(metadata_selector)
62
      |> maybe_put_virtual_keys(meta, metadata_selector)
63
      |> maybe_update(:file, &IO.chardata_to_string/1)
64

65
    line =
329✔
66
      %{
67
        time: utc_time(meta),
68
        severity: Atom.to_string(level),
69
        message: encode(message, redactors),
70
        metadata: encode(metadata, redactors)
71
      }
72
      |> maybe_put(:request, format_http_request(meta))
73
      |> maybe_put(:span, format_span(meta))
74
      |> maybe_put(:trace, format_trace(meta))
75
      |> @encoder.encode_to_iodata!(encoder_opts)
76

77
    [line, "\n"]
78
  end
79

80
  @doc false
81
  def format_binary_message(binary) do
82
    IO.chardata_to_string(binary)
131✔
83
  end
84

85
  @doc false
86
  def format_structured_message(map) when is_map(map) do
87
    map
101✔
88
  end
89

90
  def format_structured_message(keyword) do
91
    Enum.into(keyword, %{})
96✔
92
  end
93

94
  @doc false
95
  def format_crash_reason(binary, _reason, _meta) do
96
    IO.chardata_to_string(binary)
1✔
97
  end
98

99
  if Code.ensure_loaded?(Plug.Conn) do
100
    defp format_http_request(%{conn: %Plug.Conn{} = conn}) do
101
      %{
5✔
102
        connection: %{
103
          protocol: Plug.Conn.get_http_protocol(conn),
104
          method: conn.method,
5✔
105
          path: conn.request_path,
5✔
106
          status: conn.status
5✔
107
        },
108
        client: %{
109
          user_agent: Formatter.Plug.get_header(conn, "user-agent"),
110
          ip: Formatter.Plug.remote_ip(conn)
111
        }
112
      }
113
    end
114
  end
115

116
  defp format_http_request(_meta), do: nil
324✔
117

118
  # Computes virtual metadata keys from :mfa when explicitly requested via a key
119
  # list but not already present in metadata. This mirrors the special keys
120
  # supported by Elixir's built-in Logger.Formatter (see `compute_meta/2`):
121
  # https://hexdocs.pm/logger/Logger.html#module-metadata
122
  @virtual_metadata_keys [:module, :function, :initial_call]
123

124
  defp maybe_put_virtual_keys(metadata, meta, keys) when is_list(keys) do
125
    Enum.reduce(keys, metadata, &maybe_compute_meta(&1, &2, meta))
4✔
126
  end
127

128
  defp maybe_put_virtual_keys(metadata, meta, _selector) do
129
    Enum.reduce(@virtual_metadata_keys, metadata, &maybe_compute_meta(&1, &2, meta))
325✔
130
  end
131

132
  defp maybe_compute_meta(:module, acc, %{mfa: {mod, _, _}}) do
133
    Map.put_new(acc, :module, mod)
325✔
134
  end
135

136
  defp maybe_compute_meta(:function, acc, %{mfa: {_, fun, arity}}) do
137
    Map.put_new(acc, :function, "#{fun}/#{arity}")
325✔
138
  end
139

140
  defp maybe_compute_meta(:initial_call, acc, %{initial_call: {mod, fun, arity}}) do
NEW
141
    Map.put_new(acc, :initial_call, Exception.format_mfa(mod, fun, arity))
×
142
  end
143

144
  defp maybe_compute_meta(_key, acc, _meta), do: acc
331✔
145

146
  defp format_span(%{otel_span_id: otel_span_id}), do: IO.chardata_to_string(otel_span_id)
1✔
147
  defp format_span(%{span_id: span_id}), do: span_id
1✔
148
  defp format_span(_meta), do: nil
327✔
149

150
  defp format_trace(%{otel_trace_id: otel_trace_id}), do: IO.chardata_to_string(otel_trace_id)
1✔
151
  defp format_trace(%{trace_id: trace_id}), do: trace_id
1✔
152
  defp format_trace(_meta), do: nil
327✔
153
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc