Skip to content

Commit 2e6fa25

Browse files
authored
Added basic formatter and refactored common functions (#35)
* Added basic formatter and refactored common functions * Added tests for basic logger and made it the default * Fixed tests and added option to disable callback * Removed unused import * Setting formatter in ecto+plug tests * Trying to fix test race condition * Fixed testing errors * Fixing Elixir 1.6 formatting error
1 parent d01f0bc commit 2e6fa25

File tree

10 files changed

+239
-90
lines changed

10 files changed

+239
-90
lines changed

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,23 @@ After adding this back-end you may also be interested in [redirecting otp and sa
2121

2222
## Log Format
2323

24-
By-default, generated JSON is compatible with
24+
LoggerJSON provides two JSON formatters out of the box (see below for implementing your own custom formatter).
25+
26+
The `LoggerJSON.Formatters.BasicLogger` formatter provides a generic JSON formatted message with no vendor specific entries in the payload. A sample log entry from `LoggerJSON.Formatters.BasicLogger` looks like the following:
27+
28+
```json
29+
30+
```
31+
32+
The other formatter that comes with LoggerJSON is `LoggerJSON.Formatters.GoogleCloudLogger` and generates JSON that is compatible with the
2533
[Google Cloud Logger LogEntry](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry) format:
2634

2735
```json
2836
{
2937
"log":"hello",
3038
"logging.googleapis.com/sourceLocation":{
3139
"file":"/os/logger_json/test/unit/logger_json_test.exs",
32-
"function":"Elixir.LoggerJSONTest.test metadata can be configured/1",
40+
"function":"Elixir.LoggerJSONGoogleTest.test metadata can be configured/1",
3341
"line":71
3442
},
3543
"severity":"DEBUG",
@@ -40,7 +48,6 @@ By-default, generated JSON is compatible with
4048

4149
Log entry in Google Cloud Logger would looks something like this:
4250

43-
4451
```json
4552
{
4653
"httpRequest":{

lib/logger_json.ex

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ defmodule LoggerJSON do
1616
"log":"hello",
1717
"logging.googleapis.com/sourceLocation":{
1818
"file":"/os/logger_json/test/unit/logger_json_test.exs",
19-
"function":"Elixir.LoggerJSONTest.test metadata can be configured/1",
19+
"function":"Elixir.LoggerJSONGoogleTest.test metadata can be configured/1",
2020
"line":71
2121
},
2222
"severity":"DEBUG",
@@ -26,7 +26,8 @@ defmodule LoggerJSON do
2626
```
2727
2828
You can change this structure by implementing `LoggerJSON.Formatter` behaviour and passing module
29-
name to `:formatter` config option. Example module can be found in `LoggerJSON.Formatters.GoogleCloudLogger`.
29+
name to `:formatter` config option. Example implementations can be found in `LoggerJSON.Formatters.GoogleCloudLogger`
30+
and `LoggerJSON.Formatters.BasicLogger`.
3031
3132
```elixir
3233
config :logger_json, :backend,
@@ -50,7 +51,9 @@ defmodule LoggerJSON do
5051
For dynamically configuring the endpoint, such as loading data
5152
from environment variables or configuration files, LoggerJSON provides
5253
an `:on_init` option that allows developers to set a module, function
53-
and list of arguments that is invoked when the endpoint starts.
54+
and list of arguments that is invoked when the endpoint starts. If you
55+
would like to disable the `:on_init` callback function dynamically, you
56+
can pass in `:disabled` and no callback function will be called.
5457
5558
```elixir
5659
config :logger_json, :backend,
@@ -121,6 +124,7 @@ defmodule LoggerJSON do
121124
def handle_call({:configure, options}, state) do
122125
config = configure_merge(get_env(), options)
123126
put_env(config)
127+
124128
{:ok, :ok, init(config, state)}
125129
end
126130

@@ -193,6 +197,9 @@ defmodule LoggerJSON do
193197
{:ok, conf} = apply(mod, fun, [config | args])
194198
conf
195199

200+
{:ok, :disabled} ->
201+
config
202+
196203
{:ok, other} ->
197204
raise ArgumentError,
198205
"invalid :on_init option for :logger_json application. " <>
@@ -203,7 +210,7 @@ defmodule LoggerJSON do
203210
end
204211

205212
json_encoder = Keyword.get(config, :json_encoder, Jason)
206-
formatter = Keyword.get(config, :formatter, LoggerJSON.Formatters.GoogleCloudLogger)
213+
formatter = Keyword.get(config, :formatter, LoggerJSON.Formatters.BasicLogger)
207214
level = Keyword.get(config, :level)
208215
device = Keyword.get(config, :device, :user)
209216
max_buffer = Keyword.get(config, :max_buffer, 32)

lib/logger_json/formatter.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ defmodule LoggerJSON.Formatter do
22
@moduledoc """
33
Behaviour that should be implemented by log formatters.
44
5-
Example implementation can be found in `LoggerJSON.Formatters.GoogleCloudLogger`.
5+
Example implementations can be found in `LoggerJSON.Formatters.GoogleCloudLogger` and
6+
`LoggerJSON.Formatters.BasicLogger`.
67
"""
78

89
@doc """

lib/logger_json/formatter_utils.ex

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
defmodule LoggerJSON.FormatterUtils do
2+
@moduledoc """
3+
This module contains functions that can be used across different
4+
`LoggerJSON.Formatter` implementations to provide common functionality.
5+
"""
6+
7+
import Jason.Helpers, only: [json_map: 1]
8+
9+
@doc """
10+
Format an exception for use within a log entry
11+
"""
12+
def format_process_crash(md) do
13+
if crash_reason = Keyword.get(md, :crash_reason) do
14+
initial_call = Keyword.get(md, :initial_call)
15+
16+
json_map(
17+
initial_call: format_initial_call(initial_call),
18+
reason: format_crash_reason(crash_reason)
19+
)
20+
end
21+
end
22+
23+
@doc """
24+
RFC3339 UTC "Zulu" format
25+
"""
26+
def format_timestamp({date, time}) do
27+
[format_date(date), ?T, format_time(time), ?Z]
28+
|> IO.iodata_to_binary()
29+
end
30+
31+
@doc """
32+
Provide a string output of the MFA log entry
33+
"""
34+
def format_function(nil, function), do: function
35+
def format_function(module, function), do: "#{module}.#{function}"
36+
def format_function(module, function, arity), do: "#{module}.#{function}/#{arity}"
37+
38+
@doc """
39+
"""
40+
def maybe_put(map, _key, nil), do: map
41+
def maybe_put(map, key, value), do: Map.put(map, key, value)
42+
43+
defp format_initial_call(nil), do: nil
44+
defp format_initial_call({module, function, arity}), do: format_function(module, function, arity)
45+
46+
defp format_crash_reason({:throw, reason}) do
47+
Exception.format(:throw, reason)
48+
end
49+
50+
defp format_crash_reason({:exit, reason}) do
51+
Exception.format(:exit, reason)
52+
end
53+
54+
defp format_crash_reason({%{} = exception, stacktrace}) do
55+
Exception.format(:error, exception, stacktrace)
56+
end
57+
58+
defp format_crash_reason(other) do
59+
inspect(other)
60+
end
61+
62+
defp format_time({hh, mi, ss, ms}) do
63+
[pad2(hh), ?:, pad2(mi), ?:, pad2(ss), ?., pad3(ms)]
64+
end
65+
66+
defp format_date({yy, mm, dd}) do
67+
[Integer.to_string(yy), ?-, pad2(mm), ?-, pad2(dd)]
68+
end
69+
70+
defp pad3(int) when int < 10, do: [?0, ?0, Integer.to_string(int)]
71+
defp pad3(int) when int < 100, do: [?0, Integer.to_string(int)]
72+
defp pad3(int), do: Integer.to_string(int)
73+
74+
defp pad2(int) when int < 10, do: [?0, Integer.to_string(int)]
75+
defp pad2(int), do: Integer.to_string(int)
76+
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
defmodule LoggerJSON.Formatters.BasicLogger do
2+
@moduledoc """
3+
Basic JSON log formatter with no vender specific formatting
4+
"""
5+
6+
import Jason.Helpers, only: [json_map: 1]
7+
8+
alias LoggerJSON.FormatterUtils
9+
10+
@behaviour LoggerJSON.Formatter
11+
12+
@processed_metadata_keys ~w[pid file line function module application]a
13+
14+
@impl true
15+
def format_event(level, msg, ts, md, md_keys) do
16+
json_map(
17+
time: FormatterUtils.format_timestamp(ts),
18+
severity: Atom.to_string(level),
19+
message: IO.iodata_to_binary(msg),
20+
metadata: format_metadata(md, md_keys)
21+
)
22+
end
23+
24+
defp format_metadata(md, md_keys) do
25+
md
26+
|> LoggerJSON.take_metadata(md_keys, @processed_metadata_keys)
27+
|> FormatterUtils.maybe_put(:error, FormatterUtils.format_process_crash(md))
28+
end
29+
end

lib/logger_json/formatters/google_cloud_logger.ex

Lines changed: 8 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ defmodule LoggerJSON.Formatters.GoogleCloudLogger do
44
"""
55
import Jason.Helpers, only: [json_map: 1]
66

7+
alias LoggerJSON.FormatterUtils
8+
79
@behaviour LoggerJSON.Formatter
810

911
@processed_metadata_keys ~w[pid file line function module application]a
@@ -13,7 +15,6 @@ defmodule LoggerJSON.Formatters.GoogleCloudLogger do
1315
@severity_levels [
1416
{:debug, "DEBUG"},
1517
{:info, "INFO"},
16-
{:warning, "WARNING"},
1718
{:warn, "WARNING"},
1819
{:error, "ERROR"}
1920
]
@@ -28,7 +29,7 @@ defmodule LoggerJSON.Formatters.GoogleCloudLogger do
2829
def format_event(unquote(level), msg, ts, md, md_keys) do
2930
Map.merge(
3031
%{
31-
time: format_timestamp(ts),
32+
time: FormatterUtils.format_timestamp(ts),
3233
severity: unquote(gcp_level),
3334
message: IO.iodata_to_binary(msg)
3435
},
@@ -40,7 +41,7 @@ defmodule LoggerJSON.Formatters.GoogleCloudLogger do
4041
def format_event(_level, msg, ts, md, md_keys) do
4142
Map.merge(
4243
%{
43-
time: format_timestamp(ts),
44+
time: FormatterUtils.format_timestamp(ts),
4445
severity: "DEFAULT",
4546
message: IO.iodata_to_binary(msg)
4647
},
@@ -50,56 +51,17 @@ defmodule LoggerJSON.Formatters.GoogleCloudLogger do
5051

5152
defp format_metadata(md, md_keys) do
5253
LoggerJSON.take_metadata(md, md_keys, @processed_metadata_keys)
53-
|> maybe_put(:error, format_process_crash(md))
54-
|> maybe_put(:"logging.googleapis.com/sourceLocation", format_source_location(md))
55-
|> maybe_put(:"logging.googleapis.com/operation", format_operation(md))
54+
|> FormatterUtils.maybe_put(:error, FormatterUtils.format_process_crash(md))
55+
|> FormatterUtils.maybe_put(:"logging.googleapis.com/sourceLocation", format_source_location(md))
56+
|> FormatterUtils.maybe_put(:"logging.googleapis.com/operation", format_operation(md))
5657
end
5758

58-
defp maybe_put(map, _key, nil), do: map
59-
defp maybe_put(map, key, value), do: Map.put(map, key, value)
60-
6159
defp format_operation(md) do
6260
if request_id = Keyword.get(md, :request_id) do
6361
json_map(id: request_id)
6462
end
6563
end
6664

67-
defp format_process_crash(md) do
68-
if crash_reason = Keyword.get(md, :crash_reason) do
69-
initial_call = Keyword.get(md, :initial_call)
70-
71-
json_map(
72-
initial_call: format_initial_call(initial_call),
73-
reason: format_crash_reason(crash_reason)
74-
)
75-
end
76-
end
77-
78-
defp format_initial_call(nil), do: nil
79-
defp format_initial_call({module, function, arity}), do: format_function(module, function, arity)
80-
81-
defp format_crash_reason({:throw, reason}) do
82-
Exception.format(:throw, reason)
83-
end
84-
85-
defp format_crash_reason({:exit, reason}) do
86-
Exception.format(:exit, reason)
87-
end
88-
89-
defp format_crash_reason({%{} = exception, stacktrace}) do
90-
Exception.format(:error, exception, stacktrace)
91-
end
92-
93-
defp format_crash_reason(other) do
94-
inspect(other)
95-
end
96-
97-
# RFC3339 UTC "Zulu" format
98-
defp format_timestamp({date, time}) do
99-
[format_date(date), ?T, format_time(time), ?Z]
100-
|> IO.iodata_to_binary()
101-
end
102-
10365
# Description can be found in Google Cloud Logger docs;
10466
# https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogEntrySourceLocation
10567
defp format_source_location(metadata) do
@@ -111,26 +73,7 @@ defmodule LoggerJSON.Formatters.GoogleCloudLogger do
11173
json_map(
11274
file: file,
11375
line: line,
114-
function: format_function(module, function)
76+
function: FormatterUtils.format_function(module, function)
11577
)
11678
end
117-
118-
defp format_function(nil, function), do: function
119-
defp format_function(module, function), do: "#{module}.#{function}"
120-
defp format_function(module, function, arity), do: "#{module}.#{function}/#{arity}"
121-
122-
defp format_time({hh, mi, ss, ms}) do
123-
[pad2(hh), ?:, pad2(mi), ?:, pad2(ss), ?., pad3(ms)]
124-
end
125-
126-
defp format_date({yy, mm, dd}) do
127-
[Integer.to_string(yy), ?-, pad2(mm), ?-, pad2(dd)]
128-
end
129-
130-
defp pad3(int) when int < 10, do: [?0, ?0, Integer.to_string(int)]
131-
defp pad3(int) when int < 100, do: [?0, Integer.to_string(int)]
132-
defp pad3(int), do: Integer.to_string(int)
133-
134-
defp pad2(int) when int < 10, do: [?0, Integer.to_string(int)]
135-
defp pad2(int), do: Integer.to_string(int)
13679
end

test/unit/logger_json/ecto_test.exs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
defmodule LoggerJSON.EctoTest do
2-
use Logger.Case
2+
use Logger.Case, async: false
33
import ExUnit.CaptureIO
44
require Logger
55

66
setup do
7-
on_exit(fn ->
8-
:ok = Logger.configure_backend(LoggerJSON, device: :user, level: nil, metadata: [], json_encoder: Jason)
9-
end)
7+
:ok =
8+
Logger.configure_backend(
9+
LoggerJSON,
10+
device: :user,
11+
level: nil,
12+
metadata: [],
13+
json_encoder: Jason,
14+
on_init: :disabled,
15+
formatter: LoggerJSON.Formatters.GoogleCloudLogger
16+
)
1017

1118
diff = :erlang.convert_time_unit(1, :microsecond, :native)
1219

test/unit/logger_json/plug_test.exs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule LoggerJSON.PlugTest do
2-
use Logger.Case
2+
use Logger.Case, async: false
33
use Plug.Test
44
import ExUnit.CaptureIO
55
require Logger
@@ -16,11 +16,16 @@ defmodule LoggerJSON.PlugTest do
1616
end
1717

1818
setup do
19-
on_exit(fn ->
20-
:ok = Logger.configure_backend(LoggerJSON, device: :user, level: nil, metadata: [], json_encoder: Jason)
21-
end)
22-
23-
Logger.configure_backend(LoggerJSON, device: :standard_error, metadata: :all)
19+
:ok =
20+
Logger.configure_backend(
21+
LoggerJSON,
22+
device: :standard_error,
23+
level: nil,
24+
metadata: :all,
25+
json_encoder: Jason,
26+
on_init: :disabled,
27+
formatter: LoggerJSON.Formatters.GoogleCloudLogger
28+
)
2429
end
2530

2631
test "logs request information" do

0 commit comments

Comments
 (0)