-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add middleware option for slicer module and a UTF-8 aware slicer (#23)
* Allow passing the slicer module in as an option to the Tesla middleware * Small tweaks * Add UTF-8 aware slicer to allow slicing long messages that can still be used in JSON. * Rearrange slicer behaviour and slicers. * Update the specs. * Add open line between @behaviour and @doc for the first function. Co-authored-by: Felipe Vieira <[email protected]> * Instead of different function clauses use one with multiple when guards. Co-authored-by: Felipe Vieira <[email protected]> * Fix PR feedback issues. * Change example invalid atom argument in test. * Fix the test value in slicer. * Update the changelog. --------- Co-authored-by: Felipe Vieira <[email protected]>
- Loading branch information
1 parent
24377d1
commit 1daaa11
Showing
8 changed files
with
246 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,46 +1,9 @@ | ||
defmodule MetaLogger.Slicer do | ||
@moduledoc """ | ||
Responsible for slicing log entries according to the given max length option. | ||
A bebaviour for slicing long entries into a list of entries shorter than a passed `max_entry_length` value. | ||
""" | ||
|
||
@typedoc "Max length in bytes or `:infinity` if the entry should not be sliced." | ||
@type max_entry_length :: non_neg_integer() | :infinity | ||
|
||
@doc """ | ||
Returns sliced log entries according to the given max entry length. | ||
If the entry is smaller than given max length, or if `:infinity ` is given | ||
as option, a list with one entry is returned. Otherwise a list with multiple | ||
entries is returned. | ||
## Examples | ||
iex> #{inspect(__MODULE__)}.slice("1234567890", 10) | ||
["1234567890"] | ||
iex> #{inspect(__MODULE__)}.slice("1234567890", :infinity) | ||
["1234567890"] | ||
iex> #{inspect(__MODULE__)}.slice("1234567890", 5) | ||
["12345", "67890"] | ||
""" | ||
@spec slice(String.t(), max_entry_length()) :: [String.t()] | ||
def slice(entry, max_entry_length) | ||
when max_entry_length == :infinity | ||
when byte_size(entry) <= max_entry_length, | ||
do: [entry] | ||
|
||
def slice(entry, max_entry_length) do | ||
entry_length = byte_size(entry) | ||
rem = rem(entry_length, max_entry_length) | ||
sliced_entries = for <<slice::binary-size(max_entry_length) <- entry>>, do: slice | ||
|
||
if rem > 0 do | ||
remainder_entry = binary_part(entry, entry_length, rem * -1) | ||
sliced_entries ++ [remainder_entry] | ||
else | ||
sliced_entries | ||
end | ||
end | ||
@callback slice(binary(), max_entry_length()) :: [binary()] | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
defmodule MetaLogger.Slicer.DefaultImpl do | ||
@moduledoc """ | ||
Responsible for slicing log entries according to the given max length option. | ||
""" | ||
|
||
@behaviour MetaLogger.Slicer | ||
|
||
@doc """ | ||
Returns sliced log entries according to the given max entry length. | ||
If the entry is smaller than given max length, or if `:infinity ` is given | ||
as option, a list with one entry is returned. Otherwise a list with multiple | ||
entries is returned. | ||
## Examples | ||
iex> #{inspect(__MODULE__)}.slice("1234567890", 10) | ||
["1234567890"] | ||
iex> #{inspect(__MODULE__)}.slice("1234567890", :infinity) | ||
["1234567890"] | ||
iex> #{inspect(__MODULE__)}.slice("1234567890", 5) | ||
["12345", "67890"] | ||
""" | ||
@impl MetaLogger.Slicer | ||
@spec slice(binary(), MetaLogger.Slicer.max_entry_length()) :: [binary()] | ||
def slice(entry, max_entry_length) | ||
when max_entry_length == :infinity | ||
when byte_size(entry) <= max_entry_length, | ||
do: [entry] | ||
|
||
def slice(entry, max_entry_length) do | ||
entry_length = byte_size(entry) | ||
rem = rem(entry_length, max_entry_length) | ||
sliced_entries = for <<slice::binary-size(max_entry_length) <- entry>>, do: slice | ||
|
||
if rem > 0 do | ||
remainder_entry = binary_part(entry, entry_length, rem * -1) | ||
sliced_entries ++ [remainder_entry] | ||
else | ||
sliced_entries | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
defmodule MetaLogger.Slicer.Utf8Impl do | ||
@moduledoc """ | ||
Slices a string into chunks of a given length, taking into account the UTF-8 encoding of the string. | ||
""" | ||
|
||
@behaviour MetaLogger.Slicer | ||
|
||
@doc """ | ||
Returns sliced log entries according to the given max entry length. | ||
Ensures that all slices are valid UTF-8 strings by not splitting multibyte characters. | ||
If the entry is smaller than the given max length, or if `:infinity` is given | ||
as an option, a list with one entry is returned. Otherwise, a list with multiple | ||
entries is returned. | ||
## Examples | ||
iex> #{inspect(__MODULE__)}.slice("1234567890", 10) | ||
["1234567890"] | ||
iex> #{inspect(__MODULE__)}.slice("1234567890", :infinity) | ||
["1234567890"] | ||
iex> #{inspect(__MODULE__)}.slice("1234567890", 5) | ||
["12345", "67890"] | ||
iex> #{inspect(__MODULE__)}.slice("Hello 世界", 7) | ||
["Hello ", "世界"] | ||
""" | ||
@impl MetaLogger.Slicer | ||
@spec slice(binary(), MetaLogger.Slicer.max_entry_length()) :: [String.t()] | ||
def slice(entry, max_entry_length) | ||
when max_entry_length == :infinity | ||
when byte_size(entry) <= max_entry_length, | ||
do: [entry] | ||
|
||
def slice(entry, max_entry_length) do | ||
do_slice(entry, max_entry_length, [], [], 0) | ||
end | ||
|
||
@spec do_slice(binary(), integer(), [binary()], [iodata()], integer()) :: [String.t()] | ||
defp do_slice(<<>>, _max_length, slices, partial_slice, _partial_size) do | ||
# The remaining log entry is empty so we clean up the last partial_slice | ||
# and return the slices. | ||
partial_slice | ||
|> case do | ||
[] -> slices | ||
_ -> bank_partial_slice(slices, partial_slice) | ||
end | ||
|> Enum.reverse() | ||
end | ||
|
||
defp do_slice( | ||
<<codepoint::utf8, rest::binary>>, | ||
max_length, | ||
slices, | ||
partial_slice, | ||
partial_size | ||
) do | ||
codepoint_binary = <<codepoint::utf8>> | ||
codepoint_size = byte_size(codepoint_binary) | ||
new_size = partial_size + codepoint_size | ||
|
||
if new_size <= max_length do | ||
# There is still room in the partial_slice for more codepoints | ||
# so we prepend (to later reverse) the codepoint binary | ||
# and consider the next codepoint. | ||
do_slice(rest, max_length, slices, [codepoint_binary | partial_slice], new_size) | ||
else | ||
# Adding the new codepoint to the partial slice puts it over the limit | ||
# So we bank the partial slice and start the codepoint as the new partial_slice | ||
slices = bank_partial_slice(slices, partial_slice) | ||
new_partial_slice = [codepoint_binary] | ||
do_slice(rest, max_length, slices, new_partial_slice, codepoint_size) | ||
end | ||
end | ||
|
||
# Converts the inverted list of codepoints into a | ||
# binary slice and appends it to our list of slices. | ||
@spec bank_partial_slice([binary()], [iodata()]) :: [binary()] | ||
defp bank_partial_slice(slices, partial_slice) do | ||
[reconstruct_current_slice_as_binary(partial_slice) | slices] | ||
end | ||
|
||
@spec reconstruct_current_slice_as_binary([iodata()]) :: binary() | ||
defp reconstruct_current_slice_as_binary(current_slice) do | ||
IO.iodata_to_binary(Enum.reverse(current_slice)) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
defmodule MetaLogger.Slicer.DefaultImplTest do | ||
use ExUnit.Case, async: true | ||
|
||
alias MetaLogger.Slicer.DefaultImpl, as: Subject | ||
|
||
doctest Subject | ||
|
||
describe "slice/2" do | ||
setup do | ||
entry = "0123456789" | ||
|
||
{:ok, entry: entry} | ||
end | ||
|
||
test "when `:infinity` is given as max entry length, returns a list with one entry", %{ | ||
entry: entry | ||
} do | ||
assert Subject.slice(entry, :infinity) == [entry] | ||
end | ||
|
||
test "when max entry length is smaller than the size of given entry, " <> | ||
"returns a list with one entry", | ||
%{ | ||
entry: entry | ||
} do | ||
assert Subject.slice(entry, 10) == ["0123456789"] | ||
end | ||
|
||
test "when max entry length is half the size of given entry, returns a list with two entries", | ||
%{ | ||
entry: entry | ||
} do | ||
assert Subject.slice(entry, 5) == ["01234", "56789"] | ||
end | ||
|
||
test "when given max entry length is three and the given entry size is 10, " <> | ||
"returns a list with four entries", | ||
%{ | ||
entry: entry | ||
} do | ||
assert Subject.slice(entry, 3) == ["012", "345", "678", "9"] | ||
end | ||
|
||
test "when an invalid max entry length is given, returns a list with one entry", %{ | ||
entry: entry | ||
} do | ||
assert Subject.slice(entry, :foo) == [entry] | ||
end | ||
|
||
test "when slicing a UTF-8 string not all slices will be valid UTF-8 strings" do | ||
range_of_slices = | ||
7..1 | ||
|> Enum.flat_map(fn max_length -> | ||
Subject.slice("Hello 世界", max_length) | ||
end) | ||
|
||
refute Enum.all?(range_of_slices, &String.valid?/1) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters