| `[log.transid]` | transaction lag, commit boundaries, idle scheduling |
| `[log.report]` | periodic stats, status output, reporters |
| `[log.hash]` | hash table LRU eviction, fill rate, persistence |
+| `[log.filter]` | filter chain verdicts per extent, condition-by-condition traces |
All categories default to `[log.*] level = 5` (warnings-and-errors
only). Common opt-ins:
expected duplicates are being deduplicated.
* `[log.plan] level = 7` — see candidate-by-candidate planner decisions.
Useful for diagnosing why an expected dedupe didn't happen.
+* `[log.filter] level = 7` — one line per filter chain evaluation naming
+ the extent, the dedupe role (`src` or `dst`), the verdict, and the rule
+ that decided it. Useful for confirming that a new filter rule matches
+ what you meant it to. `level = 8` adds the condition-by-condition
+ trace for every evaluation, which is several lines per extent. Both
+ are high volume: the chain runs once per extent per role. The
+ `filter_*` [event counters](event-counters.md#filter) give the same
+ information in aggregate at no cost.
### Example
* `extent_zero`: An ioctl call to `LOGICAL_INO` succeeded, but reported an empty list of extents.
* `extent_zero_ref`: A start extent had zero references (extent freed or all refs stale) immediately after it was locked in the planner.
+filter
+------
+
+The `filter` event group counts filter chain verdicts. A chain is evaluated
+once per extent per dedupe role (`src` or `dst`) per planner pass, so these
+counters show whether a configured filter rule is deciding anything. Names
+are built from the role, the verdict, and the name of the rule that ended
+the chain, so the set of counters depends on the configuration:
+
+ * `filter_src_accept`, `filter_src_reject`, `filter_dst_accept`, `filter_dst_reject`: Total verdicts by role.
+ * `filter_<role>_<verdict>_<rule>`: The same verdicts split by the deciding rule, e.g. `filter_dst_reject_common.datacow` is one NODATACOW extent rejected as a dedupe destination by the built-in `common.datacow` rule. A rule whose `match-action` is `NEXT` never decides a verdict and so never appears here.
+ * `filter_<role>_<verdict>_ACCEPT`, `filter_<role>_<verdict>_REJECT`: Extents that fell through every rule to the chain's terminal token, e.g. `filter_src_accept_ACCEPT` is an extent no rule rejected.
+
+Per-extent detail is available from the `[log.filter]` log category (see
+[bees Configuration File Reference](config-file.md#categories)).
+
hash
----
# The [log.*] section sets the default level for every per-category log
# slot. Each named [log.<cat>] subsection can override its own level;
# unspecified categories inherit from [log.*]. Categories: plan, exec,
-# sys, scan, transid, report, hash.
+# sys, scan, transid, report, hash, filter.
#
# Categorized logging is opt-in: by default every category is gated to
# 5 = LOG_NOTICE-threshold (warnings, errors, and more critical only;
"transid",
"report",
"hash",
+ "filter",
};
BeesLogConfig log_cfg;
/// A null filter chain is accepted and treated as "always accept",
/// which keeps test paths and unconfigured filter setups simple.
+#include "bees.h"
#include "bees-extent.h"
#include "bees-filter.h"
#include <cstdint>
#include <memory>
#include <optional>
+#include <sstream>
+#include <string>
#include <unordered_map>
class BeesFilterCache {
bool accept = true;
if (m_chain) {
BeesFilterState state;
- m_chain->evaluate_extent(extent, state, true, false);
+ evaluate(extent, state, true, false);
accept = state.accept;
}
result.m_accept_src = accept;
bool force = false;
if (m_chain) {
BeesFilterState state;
- m_chain->evaluate_extent(extent, state, false, true);
+ evaluate(extent, state, false, true);
accept = state.accept;
force = accept && state.force_rewrite;
}
bool m_force_rewrite = false;
};
+ /// Run the chain once for one role, then account for the verdict:
+ /// `filter_<role>_<verdict>` and `filter_<role>_<verdict>_<rule>`
+ /// event counters (rule = the deciding rule's name, or the ACCEPT /
+ /// REJECT terminal that ended the chain), one INFO line per
+ /// evaluation in the filter log category, and the condition-by-
+ /// condition trace at DEBUG. Memoization in the callers means this
+ /// runs once per (extent, role) per planner pass.
+ void evaluate(BeesExtent &extent, BeesFilterState &state, bool is_src, bool is_dst)
+ {
+ if (bees_log_cat_level[BeesLogCat::Filter] > LOG_DEBUG) {
+ std::ostringstream trace;
+ m_chain->evaluate_extent(extent, state, is_src, is_dst, trace);
+ BEESLOGC(DEBUG, Filter, trace.str());
+ } else {
+ m_chain->evaluate_extent(extent, state, is_src, is_dst);
+ }
+ const std::string role = is_src ? "src" : "dst";
+ const std::string verdict = state.accept ? "accept" : "reject";
+ const std::string rule = state.matched_rule ? state.matched_rule->name : "none";
+ const std::string counter = "filter_" + role + "_" + verdict;
+ BeesStats::s_global.add_count(counter);
+ BeesStats::s_global.add_count(counter + "_" + rule);
+ BEESLOGC(INFO, Filter, "filter " << role << " " << to_hex(extent.bytenr())
+ << " " << (state.accept ? "ACCEPT" : "REJECT")
+ << " by " << rule << " in chain '" << m_chain->spec << "'");
+ }
+
std::shared_ptr<BeesFilterChain> m_chain;
std::unordered_map<uint64_t, Entry> m_map;
};
8, // BeesLogCat::Transid
8, // BeesLogCat::Report
8, // BeesLogCat::Hash
+ 8, // BeesLogCat::Filter
};
-static_assert(BeesLogCat::Count_ == 8, "update bees_log_cat_level defaults");
+static_assert(BeesLogCat::Count_ == 9, "update bees_log_cat_level defaults");
namespace {
Report,
/// Hash table LRU eviction, fill rate, persistence.
Hash,
+ /// Filter chain evaluation results and per-condition traces.
+ Filter,
/// Sentinel: number of categories. Not a real slot.
Count_,
};