defmodule LoggerTelegramBackend.Formatter do @moduledoc false alias LoggerTelegramBackend.HTML # Telegram's sendMessage limit is 4096 characters *after entities parsing*, # i.e. display text only — HTML tags and entity encoding don't count. @max_length 4096 # Reserve this many characters so the actual log message is never starved. @reserved_for_message 50 @spec format_event(String.t(), atom, keyword) :: String.t() def format_event(message, level, metadata) do level_tag = "[#{level}]" metadata_text = format_metadata(metadata) budget = @max_length - String.length(level_tag) - separator_cost(metadata_text) metadata_text = truncate(metadata_text, max(budget - @reserved_for_message, 0)) message_text = message |> to_string() |> String.trim() |> truncate(budget - String.length(metadata_text)) render(level_tag, message_text, metadata_text) end # " " between level and message; "\n" before metadata only when present. defp separator_cost(""), do: 1 defp separator_cost(_), do: 2 defp render(level_tag, message, "") do """ #{level_tag} #{message |> HTML.escape() |> highlight_title()}\ """ end defp render(level_tag, message, metadata) do """ #{level_tag} #{message |> HTML.escape() |> highlight_title()}
#{HTML.escape(metadata)}\
"""
end
defp highlight_title(text) do
case String.split(text, "\n", parts: 2) do
[single] -> "#{single}"
[title, rest] -> "#{title}\n#{rest}"
end
end
defp format_metadata(metadata) do
Enum.map_join(metadata, "\n", fn {key, value} ->
label = key |> to_string() |> String.capitalize()
"#{label}: #{inspect(value)}"
end)
end
# NOTE: Uses String.length/1 (grapheme count). Telegram's docs say "characters"
# without specifying grapheme vs code point — we assume graphemes.
defp truncate(_str, max) when max <= 0, do: ""
defp truncate(str, max) do
if String.length(str) <= max do
str
else
String.slice(str, 0, max - 1) <> "…"
end
end
end