defmodule LogfmtEx.Encoder do
@moduledoc """
Encodes key=value pairs in logfmt.
Quotes values containing spaces and `=`. For example,
Logger.info("I am a message with spaces", string: "key=value")
will be encoded as
message="I am a message with spaces" string="key=value"
Escapes newlines, line breaks, backslashes, and carriage returns.
"""
alias LogfmtEx.ValueEncoder
@typedoc """
Metadata keys are atoms.
Message, timestamp, and level keys are strings.
"""
@type key :: String.t() | atom()
@delimiter ?=
@doc """
Encodes the given key and value into a key=value pair.
Returns `iodata/0`, an efficient data type for creating large binaries from small chunks.
A subset of keys from standard Elixir metadata are handled in special ways:
* `:domain` - encoded using `inspect/1`
* `:mfa` - formatted using `Exception.format_mfa/3`
## Options:
* `:delimiter` - defaults to =
"""
@spec encode(key(), term(), keyword()) :: iodata()
def encode(key, value, opts \\ [])
def encode(:domain, domain, opts) do
delimiter = opts |> Keyword.get(:delimiter, @delimiter)
[encode_key(:domain), delimiter, inspect(domain)]
end
def encode(:mfa, {m, f, a}, opts) do
delimiter = opts |> Keyword.get(:delimiter, @delimiter)
[encode_key(:mfa), delimiter, Exception.format_mfa(m, f, a)]
end
def encode(key, value, opts) do
delimiter = opts |> Keyword.get(:delimiter, @delimiter)
[encode_key(key), delimiter, encode_value(value)]
rescue
error -> "there was an error: #{inspect(error)}"
end
defp encode_value(""), do: ""
defp encode_value(value) do
value = value |> ValueEncoder.encode()
case infer_quotes(value) do
:unquoted -> value
:quoted -> ["\"", value, "\""]
:escaped -> ["\"", escape(value), "\""]
end
end
defp infer_quotes(value), do: infer_quotes(value, :unquoted)
defp infer_quotes(<<>>, acc), do: acc
defp infer_quotes(<<" ", rest::binary>>, _acc), do: infer_quotes(rest, :quoted)
defp infer_quotes(<<"\"", _rest::binary>>, _acc), do: :escaped
defp infer_quotes(<<"=", rest::binary>>, _acc), do: infer_quotes(rest, :quoted)
defp infer_quotes(<<"\\", rest::binary>>, :unquoted), do: infer_quotes(rest, :unquoted)
defp infer_quotes(<<"\\", rest::binary>>, :quoted), do: infer_quotes(rest, :escaped)
defp infer_quotes(<<_front, rest::binary>>, acc), do: infer_quotes(rest, acc)
defp encode_key(key) when is_atom(key), do: Atom.to_string(key)
defp encode_key(key) when is_bitstring(key), do: key
defp escape(string),
do: escape(string, "")
defp escape("", acc), do: acc
defp escape(<<"\t", rest::binary>>, acc),
do: escape(rest, <<acc::binary, "\\t">>)
defp escape(<<"\n", rest::binary>>, acc),
do: escape(rest, <<acc::binary, "\\n">>)
defp escape(<<"\r", rest::binary>>, acc),
do: escape(rest, <<acc::binary, "\\r">>)
defp escape(<<"\"", rest::binary>>, acc),
do: escape(rest, <<acc::binary, "\\\"">>)
defp escape(<<"\\", rest::binary>>, acc),
do: escape(rest, <<acc::binary, "\\\\">>)
defp escape(<<c, rest::binary>>, acc),
do: escape(rest, <<acc::binary, c>>)
end