ML-158: force single-line log output in production

Three-layer architecture:
- Logster v2 handles HTTP request and LiveView socket telemetry in logfmt format
- Custom formatter as safety net escaping any remaining embedded newlines
- Config flag (single_line_logging) gating Logster attachment to prod only

Dev environment retains multi-line format for readability.
This commit is contained in:
Claudio Ortolina
2026-05-04 15:01:10 +01:00
parent 0f19afb0aa
commit 7c6760d7db
10 changed files with 407 additions and 8 deletions
@@ -0,0 +1,143 @@
defmodule MusicLibrary.Logger.SingleLineFormatterTest do
use ExUnit.Case, async: true
alias MusicLibrary.Logger.SingleLineFormatter
@ts {{2024, 1, 15}, {14, 30, 45, 123}}
describe "environment configuration" do
test "single_line_logging is disabled in dev/test" do
refute Application.fetch_env!(:music_library, :single_line_logging)
end
test "default formatter is the standard pattern (not our custom module)" do
default_formatter = Application.get_env(:logger, :default_formatter, [])
refute match?({MusicLibrary.Logger.SingleLineFormatter, _}, default_formatter[:format])
assert is_binary(default_formatter[:format])
end
test "Phoenix.Logger is not disabled in dev/test" do
phoenix_logger = Application.get_env(:phoenix, :logger, true)
assert phoenix_logger
end
end
describe "format/4" do
test "replaces embedded newlines with escaped \\n" do
result =
SingleLineFormatter.format(:error, "line1\nline2\nline3", @ts, [])
|> IO.chardata_to_string()
assert result =~ "line1\\nline2\\nline3"
refute String.contains?(result, "\nline2")
assert String.ends_with?(result, "\n")
end
test "messages without newlines pass through unchanged" do
result =
SingleLineFormatter.format(:info, "hello world", @ts, [])
|> IO.chardata_to_string()
assert result =~ "hello world"
assert String.ends_with?(result, "\n")
end
test "handles iolist input" do
iolist = ~c"hello\nworld"
result =
SingleLineFormatter.format(:error, iolist, @ts, [])
|> IO.chardata_to_string()
assert result =~ "hello\\nworld"
assert String.ends_with?(result, "\n")
end
test "preserves metadata in output" do
result =
SingleLineFormatter.format(:info, "test message", @ts, request_id: "abc123")
|> IO.chardata_to_string()
assert result =~ "request_id=abc123"
end
test "handles empty messages" do
result =
SingleLineFormatter.format(:info, "", @ts, [])
|> IO.chardata_to_string()
assert String.ends_with?(result, "\n")
assert result =~ "[info]"
end
test "handles nil metadata" do
result =
SingleLineFormatter.format(:info, "msg", @ts, [])
|> IO.chardata_to_string()
assert result =~ "[info] msg"
end
test "output is a single line (no unescaped newlines in the message part)" do
result =
SingleLineFormatter.format(:error, "a\nb\nc", @ts, [])
|> IO.chardata_to_string()
lines = String.split(result, "\n", trim: true)
assert length(lines) == 1
end
test "includes level and timestamp" do
result =
SingleLineFormatter.format(:warning, "caution", @ts, [])
|> IO.chardata_to_string()
assert result =~ "[warning]"
assert result =~ "14:30:45.123"
end
test "handles multiple metadata keys" do
result =
SingleLineFormatter.format(:info, "msg", @ts, request_id: "req1", pid: self())
|> IO.chardata_to_string()
assert result =~ "request_id=req1"
end
test "pipe character in message is preserved" do
result =
SingleLineFormatter.format(:info, "status=200|duration=10ms", @ts, [])
|> IO.chardata_to_string()
assert result =~ "status=200|duration=10ms"
end
test "handles non-binary, non-list message via inspect fallback" do
result =
SingleLineFormatter.format(:info, %{key: "value"}, @ts, [])
|> IO.chardata_to_string()
assert result =~ "%{key: \"value\"}"
end
test "handles invalid iolist gracefully (UnicodeConversionError)" do
# Surrogate codepoint 0xD800 is not valid Unicode
result =
SingleLineFormatter.format(:error, [0xD800], @ts, [])
|> IO.chardata_to_string()
assert result =~ "[error]"
assert String.ends_with?(result, "\n")
end
test "unknown metadata keys are included via inspect" do
result =
SingleLineFormatter.format(:info, "msg", @ts, custom_key: "custom_val")
|> IO.chardata_to_string()
assert result =~ "custom_key="
assert result =~ "custom_val"
end
end
end