diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..3b45b0b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,42 @@
+---
+name: build
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - master
+jobs:
+  ci:
+    name: >
+      Run Linux-based checks and tests over ${{matrix.otp_vsn}} and ${{matrix.os}}
+    runs-on: ${{matrix.os}}
+    container:
+      image: erlang:${{matrix.otp_vsn}}
+      options: --user 1001
+    strategy:
+      matrix:
+        otp_vsn: [21.3, 22.3, 23.3, 24.0]
+        os: [ubuntu-latest]
+    steps:
+      - uses: actions/checkout@v2
+      - run: rebar3 dialyzer
+      - run: rebar3 eunit
+  ci-windows:
+    name: >
+      Run Windows-based checks and tests over ${{matrix.otp_vsn}} and ${{matrix.os}}
+    runs-on: ${{matrix.os}}
+    strategy:
+      matrix:
+        otp_vsn: [23.2, 24]
+        os: [windows-latest]
+        rebar3_vsn: ['3.16']
+    steps:
+    - uses: actions/checkout@v2
+    - uses: erlef/setup-beam@v1
+      with:
+        otp-version: ${{matrix.otp_vsn}}
+        rebar3-version: ${{matrix.rebar3_vsn}}
+    - run: rebar3 dialyzer
+    - run: rebar3 eunit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e548bdd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+.eunit
+.rebar
+*.beam
+ebin
+doc
+*.swp
+erl_crash.dump
+.project
+log
+deps
+.local_dialyzer_plt
+dialyzer_unhandled_warnings
+dialyzer_warnings
+_build/
+rebar.lock
+*.crashdump
diff --git a/README.md b/README.md
index a66302d..3a1655a 100644
--- a/README.md
+++ b/README.md
@@ -218,7 +218,7 @@ will be applied on that sink.
 
 Custom Formatting
 -----------------
-All loggers have a default formatting that can be overriden.  A formatter is any module that
+All loggers have a default formatting that can be overridden.  A formatter is any module that
 exports `format(#lager_log_message{},Config#any())`.  It is specified as part of the configuration
 for the backend:
 
@@ -653,7 +653,7 @@ record a module compiled with the lager parse transform knows about by using the
 lager:info("My state is ~p", [lager:pr(State, ?MODULE)])
 ```
 
-Often, `?MODULE` is sufficent, but you can obviously substitute that for a literal module name.
+Often, `?MODULE` is sufficient, but you can obviously substitute that for a literal module name.
 `lager:pr` also works from the shell.
 
 Colored terminal output
@@ -786,6 +786,15 @@ a black hole; nothing is passed through.  A filter of `{null, true}` means
 *everything* passes through. No other values for the null filter are valid and
 will be rejected.
 
+### Silencing filters
+A special log level, `silence` can be used together with a filter in order
+to suppress specific log output. This can be useful if a backend has been
+configured for a particular log level, but a particular set of log messages
+clutters the log. If these come from a dependency, they might be difficult
+to remove entirely, and it might not be desirable to do so in general.
+In such situations, a trace filter with log level `silence` can turn them
+off selectively, while letting other messages through as before.
+
 ### Multiple sink support
 
 If using multiple sinks, there are limitations on tracing that you
@@ -1254,7 +1263,7 @@ Example Usage:
     * Bugfix: flush_threshold not working (#449)
     * Feature: Add `node` as a formatting option (#447)
     * Documentation: Update Elixir section with information about parse_transform (#446)
-    * Bugfix: Correct default console configuation to use "[{level,info}]" instead (#445)
+    * Bugfix: Correct default console configuration to use "[{level,info}]" instead (#445)
     * Feature: Pretty print lists of records at top level and field values with lager:pr (#442)
     * Bugfix: Ignore return value of lager:dispatch_log in lager.hrl (#441)
 
@@ -1330,7 +1339,7 @@ Example Usage:
 
 3.3.0 - 16 February 2017
 
-    * Docs: Fix documentation to make 'it' unambiguous when discussing asychronous
+    * Docs: Fix documentation to make 'it' unambiguous when discussing asynchronous
       operation. (#387)
     * Test: Fix test flappiness due to insufficient sanitation between test runs (#384, #385)
     * Feature: Allow metadata only logging. (#380)
diff --git a/debian/changelog b/debian/changelog
index ef2ac6b..154b482 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+erlang-lager (3.9.2+git20220129.1.a140ea9-1) UNRELEASED; urgency=low
+
+  * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 16 Apr 2022 20:15:05 -0000
+
 erlang-lager (3.9.2-2) unstable; urgency=medium
 
   [ Debian Janitor ]
diff --git a/rebar b/rebar
new file mode 100755
index 0000000..9ccb678
Binary files /dev/null and b/rebar differ
diff --git a/rebar.config b/rebar.config
index c3a639b..f1250b4 100644
--- a/rebar.config
+++ b/rebar.config
@@ -35,7 +35,7 @@
     warn_unused_import
     % do NOT include warnings_as_errors, as rebar includes these options
     % when compiling for eunit, and at least one test module has code that
-    % is deliberatly broken and will generate an un-maskable warning
+    % is deliberately broken and will generate an un-maskable warning
 ]}.
 
 {erl_first_files, ["src/lager_util.erl"]}.
diff --git a/src/error_logger_lager_h.erl b/src/error_logger_lager_h.erl
index b6ef5a9..96d2fcf 100644
--- a/src/error_logger_lager_h.erl
+++ b/src/error_logger_lager_h.erl
@@ -420,7 +420,7 @@ format_offender(Off) ->
                 [Name, MFA, get_value(pid, Off)])
     end.
 
-%% backwards compatability shim
+%% backwards compatibility shim
 format_reason(Reason) ->
     element(2, format_reason_md(Reason)).
 
@@ -551,7 +551,7 @@ format_reason_md(Reason) ->
     {Str, _} = lager_trunc_io:print(Reason, 500),
     {[], Str}.
 
-%% backwards compatability shim
+%% backwards compatibility shim
 format_mfa(MFA) ->
     element(2, format_mfa_md(MFA)).
 
@@ -575,7 +575,7 @@ format_mfa_md([{M, F, A}| _]) ->
 format_mfa_md([{M, F, A, Props}| _]) when is_list(Props) ->
    %% this kind of weird stacktrace can be generated by a uncaught throw in a gen_server
    %% TODO we might not always want to print the first MFA we see here, often it is more helpful
-   %% to print a lower one, but it is hard to programatically decide.
+   %% to print a lower one, but it is hard to programmatically decide.
    format_mfa_md({M, F, A, Props});
 format_mfa_md(Other) ->
     {[], io_lib:format("~w", [Other])}.
diff --git a/src/lager.erl b/src/lager.erl
index 5562042..d834f3c 100644
--- a/src/lager.erl
+++ b/src/lager.erl
@@ -53,7 +53,7 @@
           count :: infinity | pos_integer(),
           format_string :: string(),
           timeout :: infinity | pos_integer(),
-          started = os:timestamp() :: erlang:timestamp() %% use os:timestamp for compatability
+          started = os:timestamp() :: erlang:timestamp() %% use os:timestamp for compatibility
          }).
 
 %% API
diff --git a/src/lager_common_test_backend.erl b/src/lager_common_test_backend.erl
index 0e03d6f..cf344a1 100644
--- a/src/lager_common_test_backend.erl
+++ b/src/lager_common_test_backend.erl
@@ -13,7 +13,7 @@
         bounce/0,
         bounce/1]).
 
-%% holds the log messages for retreival on terminate
+%% holds the log messages for retrieval on terminate
 -record(state, {level :: {mask, integer()},
                 formatter :: atom(),
                 format_config :: any(),
diff --git a/src/lager_default_formatter.erl b/src/lager_default_formatter.erl
index 8ed2647..94048dd 100644
--- a/src/lager_default_formatter.erl
+++ b/src/lager_default_formatter.erl
@@ -381,7 +381,7 @@ basic_test_() ->
                         [metadata]
                     )))
         },
-        {"Metadata can be printed in its enterity with custom seperators",
+        {"Metadata can be printed in its enterity with custom separators",
             ?_assertEqual(iolist_to_binary(["bar->2, baz->3, foo->1"]),
                 iolist_to_binary(format(lager_msg:new("Message",
                             Now,
diff --git a/src/lager_file_backend.erl b/src/lager_file_backend.erl
index 6b1ab17..040ace9 100644
--- a/src/lager_file_backend.erl
+++ b/src/lager_file_backend.erl
@@ -20,7 +20,7 @@
 
 %% @doc File backend for lager, with multiple file support.
 %% Multiple files are supported, each with the path and the loglevel being
-%% configurable. The configuration paramter for this backend is a list of
+%% configurable. The configuration parameter for this backend is a list of
 %% key-value 2-tuples. See the init() function for the available options.
 %% This backend supports external and internal log
 %% rotation and will re-open handles to files if the inode changes. It will
@@ -111,7 +111,7 @@ init([{FileName, LogLevel}, {Formatter,FormatterConfig}]) when is_list(FileName)
 init(LogFileConfig) when is_list(LogFileConfig) ->
     case validate_logfile_proplist(LogFileConfig) of
         false ->
-            %% falied to validate config
+            %% failed to validate config
             {error, {fatal, bad_config}};
         Config ->
             %% probabably a better way to do this, but whatever
diff --git a/src/lager_format.erl b/src/lager_format.erl
index 3233554..338abc2 100644
--- a/src/lager_format.erl
+++ b/src/lager_format.erl
@@ -110,7 +110,7 @@ pad_char(Fmt, Args) -> {$\s,Fmt,Args}.
 
 %% collect_cc([FormatChar], [Argument]) ->
 %%         {Control,[ControlArg],[FormatChar],[Arg]}.
-%%  Here we collect the argments for each control character.
+%%  Here we collect the arguments for each control character.
 %%  Be explicit to cause failure early.
 
 collect_cc([$w|Fmt], [A|Args]) -> {$w,[A],Fmt,Args};
diff --git a/src/lager_transform.erl b/src/lager_transform.erl
index bd03512..306469b 100644
--- a/src/lager_transform.erl
+++ b/src/lager_transform.erl
@@ -182,7 +182,7 @@ do_transform(Line, SinkName, Severity, Arguments0, Safety) ->
                  unsafe ->
                      do_log_unsafe
              end,
-    %% Wrap the call to lager:dispatch_log/6 in case that will avoid doing any work if this message is not elegible for logging
+    %% Wrap the call to lager:dispatch_log/6 in case that will avoid doing any work if this message is not eligible for logging
     %% See lager.erl (lines 89-100) for lager:dispatch_log/6
     %% case {whereis(Sink), whereis(?DEFAULT_SINK), lager_config:get({Sink, loglevel}, {?LOG_NONE, []})} of
     {'case',Line,
diff --git a/src/lager_util.erl b/src/lager_util.erl
index a9b4645..a0e8478 100644
--- a/src/lager_util.erl
+++ b/src/lager_util.erl
@@ -77,6 +77,8 @@ num_to_level(?EMERGENCY) -> emergency;
 num_to_level(?LOG_NONE)  -> none.
 
 -spec config_to_mask(atom()|string()) -> {'mask', integer()}.
+config_to_mask(silence) ->
+    silence;
 config_to_mask(Conf) ->
     Levels = config_to_levels(Conf),
     {mask, lists:foldl(fun(Level, Acc) ->
@@ -419,6 +421,8 @@ trace_acc([{Key, '<', Val}|T], Acc) ->
 
 check_traces(_, _,  [], Acc) ->
     lists:flatten(Acc);
+check_traces(Attrs, Level, [{_, silence, _} = Flow|Flows], Acc) ->
+    check_traces(Attrs, Level, Flows, [check_trace(Attrs, Flow)|Acc]);
 check_traces(Attrs, Level, [{_, {mask, FilterLevel}, _}|Flows], Acc) when (Level band FilterLevel) == 0 ->
     check_traces(Attrs, Level, Flows, Acc);
 check_traces(Attrs, Level, [{Filter, _, _}|Flows], Acc) when length(Attrs) < length(Filter) ->
@@ -429,13 +433,16 @@ check_traces(Attrs, Level, [Flow|Flows], Acc) ->
 check_trace(Attrs, {Filter, _Level, Dest}) when is_list(Filter) ->
     check_trace(Attrs, {trace_all(Filter), _Level, Dest});
 
-check_trace(Attrs, {Filter, _Level, Dest}) when is_tuple(Filter) ->
+check_trace(Attrs, {Filter, Level, Dest} = F) when is_tuple(Filter) ->
     Made = gre:make(Attrs, [list]),
     glc:handle(?DEFAULT_TRACER, Made),
     Match = glc_lib:matches(Filter, Made),
     case Match of
         true ->
-            Dest;
+            case Level of
+                silence -> {silence, Dest};
+                _        -> Dest
+            end;
         false ->
             []
     end.
@@ -445,13 +452,17 @@ is_loggable(Msg, {mask, Mask}, MyName) ->
     %% using syslog style comparison flags
     %S = lager_msg:severity_as_int(Msg),
     %?debugFmt("comparing masks ~.2B and ~.2B -> ~p~n", [S, Mask, S band Mask]),
-    (lager_msg:severity_as_int(Msg) band Mask) /= 0 orelse
-    lists:member(MyName, lager_msg:destinations(Msg));
+    (not lists:member({silence, MyName}, lager_msg:destinations(Msg)))
+        andalso
+          ((lager_msg:severity_as_int(Msg) band Mask) /= 0 orelse
+           lists:member(MyName, lager_msg:destinations(Msg)));
 is_loggable(Msg, SeverityThreshold, MyName) when is_atom(SeverityThreshold) ->
     is_loggable(Msg, level_to_num(SeverityThreshold), MyName);
 is_loggable(Msg, SeverityThreshold, MyName) when is_integer(SeverityThreshold) ->
-    lager_msg:severity_as_int(Msg) =< SeverityThreshold orelse
-    lists:member(MyName, lager_msg:destinations(Msg)).
+    (not lists:member({silence, MyName}, lager_msg:destinations(Msg)))
+        andalso
+        (lager_msg:severity_as_int(Msg) =< SeverityThreshold orelse
+         lists:member(MyName, lager_msg:destinations(Msg))).
 
 i2l(I) when I < 10  -> [$0, $0+I];
 i2l(I)              -> integer_to_list(I).
diff --git a/test/lager_test_backend.erl b/test/lager_test_backend.erl
index 78bee6c..1c7d763 100644
--- a/test/lager_test_backend.erl
+++ b/test/lager_test_backend.erl
@@ -437,9 +437,9 @@ lager_test_() ->
                         ?assertEqual(0, count()),
                         lager:trace(?MODULE, [{module, ?MODULE}], debug),
                         ?assertMatch({?ERROR bor ?CRITICAL bor ?ALERT bor ?EMERGENCY, _}, lager_config:get(loglevel)),
-                        %% elegible for tracing
+                        %% eligible for tracing
                         ok = lager:info("hello world"),
-                        %% NOT elegible for tracing
+                        %% NOT eligible for tracing
                         ok = lager:log(info, [{pid, self()}], "hello world"),
                         ?assertEqual(1, count()),
                         ok
@@ -943,7 +943,7 @@ test_body(Expected, Actual) ->
                        "Trailing data \"~s\" following \"~s\"",
                        [Rest, Expected]);
                 {match, [{0, _}]} ->
-                    % the whole sting is " line NNN"
+                    % the whole string is " line NNN"
                     ok;
                 {match, [{Off, _}]} ->
                     ?debugFmt(
diff --git a/test/lager_trace_test.erl b/test/lager_trace_test.erl
index 48d722d..098d574 100644
--- a/test/lager_trace_test.erl
+++ b/test/lager_trace_test.erl
@@ -13,6 +13,7 @@
 -define(SECOND_LOG_ENTRY_TIMEOUT, 1000). % 1 second
 
 -define(FNAME, "test/test1.log").
+-define(FNAME2, "test/test2.log").
 
 trace_test_() ->
     {timeout,
@@ -48,7 +49,8 @@ trace_test_() ->
                  application:unset_env(lager, async_threshold),
                  error_logger:tty(true)
          end,
-         [{"Trace combined with log_root",
+         [
+          {"Trace combined with log_root",
              fun() ->
                  lager:info([{tag, mytag}], "Test message"),
 
@@ -75,6 +77,77 @@ trace_test_() ->
              end}
           ]}}.
 
+trace_silence_test_() ->
+    {timeout,
+     10,
+     {foreach,
+         fun() ->
+                 file:write_file(?FNAME2, ""),
+                 error_logger:tty(false),
+                 application:load(lager),
+                 application:set_env(lager, log_root, "test"),
+                 application:set_env(lager, handlers,
+                                     [{lager_file_backend,
+                                       [{file, "test2.log"},
+                                        {level, info},
+                                        {formatter, lager_default_formatter},
+                                        {formatter_config, [message, "\n"]}
+                                       ]}]),
+                 application:set_env(lager, traces,
+                                     [{{lager_file_backend, "test2.log"},
+                                       [{tag, silencetag}], silence}]),
+                 application:set_env(lager, error_logger_redirect, false),
+                 application:set_env(lager, async_threshold, undefined),
+                 lager:start()
+         end,
+         fun(_) ->
+                 file:delete(?FNAME2),
+                 application:stop(lager),
+                 application:stop(goldrush),
+                 application:unset_env(lager, log_root),
+                 application:unset_env(lager, handlers),
+                 application:unset_env(lager, traces),
+                 application:unset_env(lager, error_logger_redirect),
+                 application:unset_env(lager, async_threshold),
+                 error_logger:tty(true)
+         end,
+         [
+          {"Trace filter silences output",
+             fun() ->
+                 %% One message is supposed to get through, the other not
+                 lager:info([{tag, mytag}], "Test message"),
+                 lager:info([{tag, silencetag}], "This shouldn't get logged"),
+
+                 % Wait until we have the expected log entry in the log file.
+                 case wait_until(fun() ->
+                                         count_lines(?FNAME2) >= 1
+                                 end, ?FIRST_LOG_ENTRY_TIMEOUT) of
+                     ok ->
+                         ok;
+                     {error, timeout} ->
+                         throw({file_empty, file:read_file(?FNAME)})
+                 end,
+
+                 % Let's wait a little to see that we don't get a duplicate log
+                 % entry.
+                 case wait_until(fun() ->
+                                         count_lines(?FNAME2) >= 2
+                                 end, ?SECOND_LOG_ENTRY_TIMEOUT) of
+                     ok ->
+                         throw({too_many_entries, file:read_file(?FNAME)});
+                     {error, timeout} ->
+                         ok
+                 end,
+                 {ok, Bin} = file:read_file(?FNAME2),
+                 case Bin of
+                     <<"Test message\n">> ->
+                         ok;
+                     _ ->
+                         throw({unexpected_output_in_log, Bin})
+                 end
+             end}
+          ]}}.
+
 % Wait until Fun() returns true.
 wait_until(Fun, Timeout) ->
     wait_until(Fun, Timeout, {8, 13}).
diff --git a/test/sync_error_logger.erl b/test/sync_error_logger.erl
index 078000f..bd1b609 100644
--- a/test/sync_error_logger.erl
+++ b/test/sync_error_logger.erl
@@ -21,7 +21,7 @@
 %% The error_logger API, but synchronous!
 %% This is helpful for tests, otherwise you need lots of nasty timer:sleep.
 %% Additionally, the warning map can be set on a per-process level, for
-%% convienience, via the process dictionary value `warning_map'.
+%% convenience, via the process dictionary value `warning_map'.
 
 -export([
         info_msg/1, info_msg/2,