Skip to content

Commit 245b462

Browse files
committed
feat: support for before_send_log callback
Closes #909
1 parent 35dfb1b commit 245b462

File tree

5 files changed

+234
-7
lines changed

5 files changed

+234
-7
lines changed

lib/sentry.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,17 @@ defmodule Sentry do
213213
(Sentry.Event.t(), result :: term() -> term())
214214
| {module(), function_name :: atom()}
215215

216+
@typedoc """
217+
A callback to use with the `:before_send_log` configuration option.
218+
219+
If this is `{module, function_name}`, then `module.function_name(log_event)` will
220+
be called, where `log_event` is of type `t:Sentry.LogEvent.t/0`.
221+
"""
222+
@typedoc since: "12.0.0"
223+
@type before_send_log_callback() ::
224+
(Sentry.LogEvent.t() -> as_boolean(Sentry.LogEvent.t()))
225+
| {module(), function_name :: atom()}
226+
216227
@typedoc """
217228
The strategy to use when sending an event to Sentry.
218229
"""

lib/sentry/client.ex

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,13 @@ defmodule Sentry.Client do
138138
@doc """
139139
Sends a batch of log events to Sentry.
140140
141-
Log events are sent asynchronously and do not support callbacks or sampling.
141+
Log events are sent asynchronously and do not support sampling.
142142
They are buffered and sent in batches according to the Sentry Logs Protocol.
143143
144+
If a `:before_send_log` callback is configured, it will be called for each log event.
145+
If the callback returns `nil` or `false`, the log event is not sent. If it returns an
146+
updated `Sentry.LogEvent`, that will be used instead.
147+
144148
Returns `{:ok, envelope_id}` on success or `{:error, reason}` on failure.
145149
"""
146150
@doc since: "12.0.0"
@@ -154,17 +158,49 @@ defmodule Sentry.Client do
154158
{:ok, ""}
155159

156160
:not_collecting ->
157-
client = Config.client()
161+
log_events
162+
|> run_before_send_log_callbacks()
163+
|> do_send_log_events()
164+
end
165+
end
158166

159-
request_retries =
160-
Application.get_env(:sentry, :request_retries, Transport.default_retries())
167+
defp run_before_send_log_callbacks(log_events) do
168+
callback = Config.before_send_log()
161169

162-
log_events
163-
|> Envelope.from_log_events()
164-
|> Transport.encode_and_post_envelope(client, request_retries)
170+
if callback do
171+
Enum.reduce(log_events, [], fn log_event, acc ->
172+
case call_before_send_log(log_event, callback) do
173+
%LogEvent{} = result -> [result | acc]
174+
_ -> acc
175+
end
176+
end)
177+
|> Enum.reverse()
178+
else
179+
log_events
165180
end
166181
end
167182

183+
defp call_before_send_log(log_event, function) when is_function(function, 1) do
184+
function.(log_event)
185+
end
186+
187+
defp call_before_send_log(log_event, {mod, fun}) do
188+
apply(mod, fun, [log_event])
189+
end
190+
191+
defp do_send_log_events([]), do: {:ok, ""}
192+
193+
defp do_send_log_events(log_events) do
194+
client = Config.client()
195+
196+
request_retries =
197+
Application.get_env(:sentry, :request_retries, Transport.default_retries())
198+
199+
log_events
200+
|> Envelope.from_log_events()
201+
|> Transport.encode_and_post_envelope(client, request_retries)
202+
end
203+
168204
defp sample_event(sample_rate) do
169205
cond do
170206
sample_rate == 1 -> :ok

lib/sentry/config.ex

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,17 @@ defmodule Sentry.Config do
603603
be passed as arguments. The return value of the callback is not returned. See the
604604
[*Event Callbacks* section](#module-event-callbacks) below for more information.
605605
"""
606+
],
607+
before_send_log: [
608+
type: {:or, [{:fun, 1}, {:tuple, [:atom, :atom]}]},
609+
type_doc: "`t:before_send_log_callback/0`",
610+
doc: """
611+
Allows performing operations on a log event *before* it is sent, as
612+
well as filtering out the log event altogether.
613+
If the callback returns `nil` or `false`, the log event is not reported. If it returns an
614+
updated `Sentry.LogEvent`, then the updated log event is used instead.
615+
*Available since v12.0.0*.
616+
"""
606617
]
607618
]
608619

@@ -808,6 +819,10 @@ defmodule Sentry.Config do
808819
@spec max_log_events() :: non_neg_integer()
809820
def max_log_events, do: fetch!(:max_log_events)
810821

822+
@spec before_send_log() ::
823+
(Sentry.LogEvent.t() -> Sentry.LogEvent.t() | nil | false) | {module(), atom()} | nil
824+
def before_send_log, do: get(:before_send_log)
825+
811826
@spec put_config(atom(), term()) :: :ok
812827
def put_config(key, value) when is_atom(key) do
813828
unless key in @valid_keys do

test/sentry/config_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,18 @@ defmodule Sentry.ConfigTest do
243243
Config.validate!(after_send_event: :not_a_function)
244244
end
245245
end
246+
247+
test ":before_send_log" do
248+
assert Config.validate!(before_send_log: {MyMod, :my_fun})[:before_send_log] ==
249+
{MyMod, :my_fun}
250+
251+
fun = & &1
252+
assert Config.validate!(before_send_log: fun)[:before_send_log] == fun
253+
254+
assert_raise ArgumentError, ~r/invalid value for :before_send_log option/, fn ->
255+
Config.validate!(before_send_log: :not_a_function)
256+
end
257+
end
246258
end
247259

248260
describe "put_config/2" do

test/sentry/logger_handler/logs_test.exs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,159 @@ defmodule Sentry.LoggerHandler.LogsTest do
618618
end
619619
end
620620

621+
describe "before_send_log callback" do
622+
test "allows modifying log events before sending", %{bypass: bypass, buffer: buffer} do
623+
test_pid = self()
624+
625+
put_test_config(
626+
before_send_log: fn log_event ->
627+
%{log_event | attributes: Map.put(log_event.attributes, "custom_attr", "injected")}
628+
end
629+
)
630+
631+
Bypass.expect_once(bypass, "POST", "/api/1/envelope/", fn conn ->
632+
{:ok, body, conn} = Plug.Conn.read_body(conn)
633+
[_header, _item_header, item_body, _] = String.split(body, "\n")
634+
635+
item_body_map = decode!(item_body)
636+
assert %{"items" => [log_event]} = item_body_map
637+
638+
assert log_event["attributes"]["custom_attr"] == %{
639+
"type" => "string",
640+
"value" => "injected"
641+
}
642+
643+
send(test_pid, :envelope_sent)
644+
Plug.Conn.resp(conn, 200, ~s<{"id": "test-123"}>)
645+
end)
646+
647+
Logger.info("Test message")
648+
649+
assert_buffer_size(buffer, 1)
650+
651+
LogEventBuffer.flush(server: buffer)
652+
653+
assert_receive :envelope_sent, 1000
654+
end
655+
656+
test "filters out log events when callback returns nil", %{bypass: bypass, buffer: buffer} do
657+
put_test_config(
658+
before_send_log: fn log_event ->
659+
if String.contains?(log_event.body, "should_be_filtered") do
660+
nil
661+
else
662+
log_event
663+
end
664+
end
665+
)
666+
667+
test_pid = self()
668+
669+
Bypass.expect_once(bypass, "POST", "/api/1/envelope/", fn conn ->
670+
{:ok, body, conn} = Plug.Conn.read_body(conn)
671+
[_header, _item_header, item_body, _] = String.split(body, "\n")
672+
673+
item_body_map = decode!(item_body)
674+
assert %{"items" => [log_event]} = item_body_map
675+
assert log_event["body"] == "This message should pass"
676+
677+
send(test_pid, :envelope_sent)
678+
Plug.Conn.resp(conn, 200, ~s<{"id": "test-123"}>)
679+
end)
680+
681+
Logger.info("This message should_be_filtered")
682+
Logger.info("This message should pass")
683+
684+
assert_buffer_size(buffer, 2)
685+
686+
LogEventBuffer.flush(server: buffer)
687+
688+
assert_receive :envelope_sent, 1000
689+
end
690+
691+
test "filters out log events when callback returns false", %{bypass: bypass, buffer: buffer} do
692+
put_test_config(
693+
before_send_log: fn log_event ->
694+
if String.contains?(log_event.body, "drop_me") do
695+
false
696+
else
697+
log_event
698+
end
699+
end
700+
)
701+
702+
test_pid = self()
703+
704+
Bypass.expect_once(bypass, "POST", "/api/1/envelope/", fn conn ->
705+
{:ok, body, conn} = Plug.Conn.read_body(conn)
706+
[_header, _item_header, item_body, _] = String.split(body, "\n")
707+
708+
item_body_map = decode!(item_body)
709+
assert %{"items" => [log_event]} = item_body_map
710+
assert log_event["body"] == "Keep this message"
711+
712+
send(test_pid, :envelope_sent)
713+
Plug.Conn.resp(conn, 200, ~s<{"id": "test-123"}>)
714+
end)
715+
716+
Logger.info("drop_me please")
717+
Logger.info("Keep this message")
718+
719+
assert_buffer_size(buffer, 2)
720+
721+
LogEventBuffer.flush(server: buffer)
722+
723+
assert_receive :envelope_sent, 1000
724+
end
725+
726+
test "supports MFA tuple callback format", %{bypass: bypass, buffer: buffer} do
727+
test_pid = self()
728+
729+
put_test_config(before_send_log: {__MODULE__, :before_send_log_callback})
730+
731+
Bypass.expect_once(bypass, "POST", "/api/1/envelope/", fn conn ->
732+
{:ok, body, conn} = Plug.Conn.read_body(conn)
733+
[_header, _item_header, item_body, _] = String.split(body, "\n")
734+
735+
item_body_map = decode!(item_body)
736+
assert %{"items" => [log_event]} = item_body_map
737+
738+
assert log_event["attributes"]["mfa_added"] == %{
739+
"type" => "string",
740+
"value" => "true"
741+
}
742+
743+
send(test_pid, :envelope_sent)
744+
Plug.Conn.resp(conn, 200, ~s<{"id": "test-123"}>)
745+
end)
746+
747+
Logger.info("Test MFA callback")
748+
749+
assert_buffer_size(buffer, 1)
750+
751+
LogEventBuffer.flush(server: buffer)
752+
753+
assert_receive :envelope_sent, 1000
754+
end
755+
756+
test "does not send any logs when all are filtered", %{buffer: buffer} do
757+
put_test_config(before_send_log: fn _log_event -> nil end)
758+
759+
Logger.info("All messages filtered 1")
760+
Logger.info("All messages filtered 2")
761+
762+
assert_buffer_size(buffer, 2)
763+
764+
LogEventBuffer.flush(server: buffer)
765+
766+
refute_receive _, 100
767+
end
768+
end
769+
770+
def before_send_log_callback(log_event) do
771+
%{log_event | attributes: Map.put(log_event.attributes, "mfa_added", "true")}
772+
end
773+
621774
defp add_logs_handler(%{buffer: buffer}) do
622775
handler_name = :"sentry_logs_handler_#{System.unique_integer([:positive])}"
623776

0 commit comments

Comments
 (0)