Skip to content

Commit

Permalink
feat(client): ability to grep based on metadata (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
levex authored Sep 12, 2023
1 parent 76a7ac3 commit 72a9c7c
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 2 deletions.
14 changes: 14 additions & 0 deletions lib/ring_logger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,20 @@ defmodule RingLogger do
@spec grep(Regex.t() | String.t(), client_options()) :: :ok | {:error, term()}
defdelegate grep(regex_or_string, opts \\ []), to: Autoclient

@doc """
Return a list of formatted log entries that match the given metadata key-value pair.
For example:
iex> RingLogger.grep_metadata(:session_id, "abc")
:ok
iex> RingLogger.grep_metadata(:session_id, ~r/something/)
:ok
"""
@spec grep_metadata(atom(), String.t() | Regex.t()) :: :ok | {:error, term()}
defdelegate grep_metadata(key, match_value), to: Autoclient

@doc """
Helper method for formatting log messages per the current client's
configuration.
Expand Down
8 changes: 8 additions & 0 deletions lib/ring_logger/autoclient.ex
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ defmodule RingLogger.Autoclient do
run(&Client.grep(&1, regex_or_string, opts), opts)
end

@doc """
Return a list of formatted log entries that match the given metadata key-value pair.
"""
@spec grep_metadata(atom(), String.t() | Regex.t()) :: :ok | {:error, term()}
def grep_metadata(key, match_value) do
run(&Client.grep_metadata(&1, key, match_value, []), [])
end

@doc """
Reset the index used to keep track of the position in the log for `tail/1` so
that the next call to `tail/1` starts back at the oldest entry.
Expand Down
43 changes: 43 additions & 0 deletions lib/ring_logger/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,28 @@ defmodule RingLogger.Client do
GenServer.call(client_pid, {:save, path})
end

@spec grep_metadata(
GenServer.server(),
atom(),
String.t() | Regex.t(),
RingLogger.client_options()
) ::
:ok | {:error, term()}
def grep_metadata(client_pid, key, match_value, opts)

def grep_metadata(client_pid, key, match_value, opts) when is_binary(match_value) do
with {:ok, regex} <- Regex.compile(match_value) do
grep_metadata(client_pid, key, regex, opts)
end
end

def grep_metadata(client_pid, key, %Regex{} = regex, opts) do
{io, to_print} = GenServer.call(client_pid, {:grep_metadata, key, regex, opts})

pager = Keyword.get(opts, :pager, &IO.binwrite/2)
pager.(io, to_print)
end

@doc """
Run a regular expression on each entry in the log and print out the matchers.
Expand Down Expand Up @@ -254,6 +276,17 @@ defmodule RingLogger.Client do
{:reply, :ok, %{state | index: 0}}
end

def handle_call({:grep_metadata, key, match_value, _opts}, _from, state) do
to_return =
for message <- Server.get(0, 0),
should_print?(message, state),
has_metadata?(message, key, match_value),
formatted = format_message(message, state),
do: IO.chardata_to_string(formatted)

{:reply, {state.io, to_return}, state}
end

def handle_call({:grep, regex, opts}, _from, state) do
formatted_buff =
for {message, i} <- Enum.with_index(Server.get(0, 0)),
Expand Down Expand Up @@ -500,4 +533,14 @@ defmodule RingLogger.Client do
[]
end
end

@spec has_metadata?(RingLogger.entry(), atom(), String.t() | Regex.t()) :: boolean()
def has_metadata?(%{metadata: metadata}, key, match_value) do
case metadata[key] do
nil -> false
val when is_binary(val) -> val =~ match_value
val when val == match_value -> true
_ -> false
end
end
end
55 changes: 53 additions & 2 deletions test/ring_logger_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ defmodule RingLoggerTest do
# The RingLogger needs to be attached for this to work. This makes
# logging synchronous so that we can test tail, next, grep, etc. that
# rely on the messages being received by RingLogger.
defp handshake_log(io, level, message) do
Logger.log(level, message)
defp handshake_log(io, level, message, metadata \\ []) do
Logger.log(level, message, metadata)
assert_receive {:io, msg}
assert String.contains?(msg, to_string(level))

Expand Down Expand Up @@ -183,6 +183,57 @@ defmodule RingLoggerTest do
refute message =~ "[debug] a3"
end

test "can grep based on metadata with exact match", %{io: io} do
:ok = RingLogger.attach(io: io)

io
|> handshake_log(:debug, "b3")
|> handshake_log(:debug, "b2")
|> handshake_log(:debug, "b1")
|> handshake_log(:debug, "howdy", session_id: "user_1")
|> handshake_log(:debug, "a1")
|> handshake_log(:debug, "a2")
|> handshake_log(:debug, "a3")

:ok = RingLogger.grep_metadata(:session_id, "user_1")
assert_receive {:io, message}
refute message =~ "[debug] b1"
refute message =~ "[debug] b2"
refute message =~ "[debug] b3"
assert message =~ "howdy"
refute message =~ "[debug] a1"
refute message =~ "[debug] a2"
refute message =~ "[debug] a3"
end

test "can grep based on metadata with regexp", %{io: io} do
:ok = RingLogger.attach(io: io)

io
|> handshake_log(:debug, "b3")
|> handshake_log(:debug, "b2")
|> handshake_log(:debug, "b1")
|> handshake_log(:debug, "howdy", session_id: "user_1")
|> handshake_log(:debug, "hello", session_id: "user_2")
|> handshake_log(:debug, "john", session_id: "irrelevant_1")
|> handshake_log(:debug, "doe", session_id: "irrelevant_2")
|> handshake_log(:debug, "a1")
|> handshake_log(:debug, "a2")
|> handshake_log(:debug, "a3")

:ok = RingLogger.grep_metadata(:session_id, ~r/user/)
assert_receive {:io, message}
refute message =~ "[debug] b1"
refute message =~ "[debug] b2"
refute message =~ "[debug] b3"
assert message =~ "howdy" or message =~ "hello"
refute message =~ "john"
refute message =~ "joe"
refute message =~ "[debug] a1"
refute message =~ "[debug] a2"
refute message =~ "[debug] a3"
end

test "invalid regex returns error", %{io: io} do
assert {:error, _} = RingLogger.grep(5, io: io)
end
Expand Down

0 comments on commit 72a9c7c

Please sign in to comment.