]> git.ipfire.org Git - thirdparty/bird.git/commitdiff
Protocol stats: a WIP implementation of conditional routes
authorVojtech Vilimek <vojtech.vilimek@nic.cz>
Wed, 13 Jul 2022 13:39:14 +0000 (15:39 +0200)
committerMaria Matejka <mq@ucw.cz>
Tue, 26 Sep 2023 08:42:28 +0000 (10:42 +0200)
Add new global counter for stats channel

New symbol is added for each stats protocol channel, the symbol is accessed by
same name as the underlying channel. Symbols evaluate to the sum of all routes
exported from connected table with generation less than max generation of
particular channel. Default max generation is 16.

Beware you shouldn't make cyclic references as the behavior of such
configuration is not defined!

17 files changed:
conf/conf.h
configure.ac
doc/bird.sgml
filter/config.Y
filter/f-inst.c
filter/filter.c
lib/timer.c
lib/timer.h
nest/proto.c
nest/protocol.h
proto/Doc
proto/stats/Doc [new file with mode: 0644]
proto/stats/Makefile [new file with mode: 0644]
proto/stats/config.Y [new file with mode: 0644]
proto/stats/stats-pub.h [new file with mode: 0644]
proto/stats/stats.c [new file with mode: 0644]
proto/stats/stats.h [new file with mode: 0644]

index 58a2733b6de2ecf7f2ef1f3128648ecc6aac03c0..a4bc1f291d832f55f427853ac9b4d73b1755d469 100644 (file)
@@ -112,6 +112,7 @@ void cfg_copy_list(list *dest, list *src, unsigned node_size);
 
 extern int (*cf_read_hook)(byte *buf, uint max, int fd);
 
+struct stats_term_config;
 struct symbol {
   node n;                              /* In list of symbols in config */
   struct symbol *next;
@@ -127,6 +128,8 @@ struct symbol {
     struct ea_class *attribute;                /* For SYM_ATTRIBUTE */
     struct f_val *val;                 /* For SYM_CONSTANT */
     uint offset;                       /* For SYM_VARIABLE */
+    struct channel_config *ch_config;  /* For SYM_COUNTER */
+    struct stats_term_config *term;     /* For SYM_COUNTER_TERM */
   };
 
   char name[0];
@@ -161,6 +164,8 @@ struct bytestring {
 #define SYM_FILTER 4
 #define SYM_TABLE 5
 #define SYM_ATTRIBUTE 6
+#define SYM_COUNTER 7
+#define SYM_COUNTER_TERM 8
 
 #define SYM_VARIABLE 0x100     /* 0x100-0x1ff are variable types */
 #define SYM_VARIABLE_RANGE SYM_VARIABLE ... (SYM_VARIABLE | 0xff)
index bf5c6df453c84210438d400186b08d0717d4a2fc..07b249093dd1f9c8af02654cedb1a52262449507 100644 (file)
@@ -307,7 +307,7 @@ if test "$enable_mpls_kernel" != no ; then
 fi
 
 # temporarily removed "mrt" from all_protocols to speed up 3.0-alpha1 release
-all_protocols="bfd babel bgp ospf perf pipe radv rip rpki static"
+all_protocols="bfd babel bgp ospf perf pipe radv rip rpki static stats"
 all_protocols=`echo $all_protocols | sed 's/ /,/g'`
 
 if test "$with_protocols" = all ; then
index 0da616c700c0265c8c70a53c811a4bd3e934bf49..c5463e64f3060138896cbf655d76a381285b136c 100644 (file)
@@ -5560,6 +5560,64 @@ protocol static {
 }
 </code>
 
+<sect>Stats
+<label id="stats">
+
+<sect1>
+<label id="stats-intro">
+
+<p>The Statistics protocol allows you to measure number of exported routes from
+a table. You can also narrow the view by applying export filter inside stats
+channel. One instance of stats protocol can have multiple channels attached.
+Stats protocol could be particullary useful when making conditional route advertising.
+
+<p>Statistics are accessed in filters by same name as channel connected to desired
+table. Value of this expresion is evaluated to the sum of all routes with
+generation smaller than max generation (see below).
+
+<sect1>Configuration
+<label id="stats-config">
+
+<p><descrtip>
+       <tag><label id="stats-max-generation">max generation <m/expr/</tag>
+       Statistics counter contains sum of all routes with generation less
+       than or equal to max generation. This copies behavior of pipe's
+       <ref id="pipe-max-generation" name="max generetion">. Must be in range
+       from 0 to 254. Default: 16.
+
+       <tab><label id="stats-min-settle-time">min settle time <m/time/ </tag>
+       Specify a minimum value of the settle time. When a stats counter changes,
+       automatic recalcualtion of route filters may be triggered, after a short
+       settle time. Minimum settle time is a delay from the last counter change
+       to wait for more updates. No action is done if counter has same value
+       before and after changes. Default: 1 s.
+
+       <tab><label id="stats-max-settle-time">max settle time <m/time/ </tag>
+       Specify a maximum value of the settle time. When stats counter changes,
+       automatic route filter recalculation may be triggered, after a short
+       settle time. Maximum settle time is an upper limit to the settle time
+       from initial counter change even if there are consecutive updates
+       gradually renewing the settle time. No action is done if counter has
+       same value before and after changes. Defualt: 20 s.
+
+</descrip>
+
+<p>Example
+<code>
+protocol stats {
+  ipv4 stats1 { table bgp_tab1; };
+}
+
+protocol static {
+  # note that the stats1 is unrelated
+  ipv4 { import where stats1 > 200; };
+  route 0.0.0.0:0;
+}
+</code>
+
+<p>Beware that configuration with cyclic references (even logical ones) are
+considered invalid and the behaviour is not defined! You <em>should</em> avoid
+them. No detection is implemented yet.
 
 <chapt>Conclusions
 <label id="conclusion">
index e6264a83c3c4b97b2dd6e9815dc117630947ee8d..25018a0ba9b8a9454ae35bf4a21840388f0822ce 100644 (file)
@@ -801,6 +801,12 @@ symbol_value: symbol_known
       case SYM_ATTRIBUTE:
        $$ = f_new_inst(FI_EA_GET, $1->attribute);
        break;
+      case SYM_COUNTER:
+       $$ = f_new_inst(FI_COUNTER, $1);
+       break;
+      case SYM_COUNTER_TERM:
+       $$ = f_new_inst(FI_COUNTER_TERM, $1);
+       break;
       default:
        cf_error("Can't get value of symbol %s", $1->name);
     }
index 4d0974e9221dc6ff4a8948603ca6062607a33df3..ba202507d8929477621fb64e2024856124e2549f 100644 (file)
     RESULT_VAL(fstk->vstk[curline.vbase + sym->offset]);
   }
 
+  INST(FI_COUNTER, 0, 1) {
+    SYMBOL;
+    NEVER_CONSTANT;
+    RESULT(T_INT, i, stats_get_counter(sym));
+  }
+
+  INST(FI_COUNTER_TERM, 0, 1) {
+    SYMBOL;
+    NEVER_CONSTANT;
+
+    RESULT_TYPE(sym->val->type);
+    RESULT_VAL(*sym->val);
+  }
+
   INST(FI_CONSTANT, 0, 1) {
     FID_MEMBER(
       struct f_val,
index 931bc85eb7dbe5298a12600283faf65f35425cc2..b922f3b945879b5be293f0138116e642208ac857 100644 (file)
@@ -43,6 +43,7 @@
 #include "filter/filter.h"
 #include "filter/f-inst.h"
 #include "filter/data.h"
+#include "proto/stats/stats-pub.h"  /* provides function get_stats_counter() used in f-inst.c */
 
 
 /* Exception bits */
index d64a3921914289f705853c5c4e6abbaaf92bffea..836e0f5c03de6e4f13a90bf8902732cc8d82ed8b 100644 (file)
@@ -325,3 +325,71 @@ tm_format_real_time(char *x, size_t max, const char *fmt, btime t)
 
   return strftime(x, max, tbuf, &tm);
 }
+
+
+/*
+ *  Settle timer
+ */
+
+static inline btime
+settled_time(struct settle_timer *st)
+{
+  ASSUME(st->base_settle_time != 0);
+
+  return MIN_(st->last_change + st->min_settle_time,
+             st->base_settle_time + st->max_settle_time);
+}
+
+inline void
+settle_timer_changed(struct settle_timer *st)
+{
+  st->last_change = current_time();
+}
+
+void
+settle_timer(timer *t)
+{
+  struct settle_timer *st = t->data;
+
+  if (!st->base_settle_time)
+    return;
+
+  btime settled_t = settled_time(st);
+  if (current_time() < settled_t)
+  {
+    tm_set(t, settled_t);
+    return;
+  }
+
+
+  /* Settled */
+  st->base_settle_time = 0;
+
+  /* t->hook already occupied by settle_timer() */
+  if (st->settle_hook)
+    st->settle_hook(st);
+}
+
+void
+stm_init(struct settle_timer *st, pool *p, void *data, 
+         void (*settle_hook)(struct settle_timer *st))
+{
+  st->t = tm_new_init(p, (void *) st, settle_timer, 0, 0); 
+  st->t->hook = settle_timer;
+  st->t->data = (void *) st;
+
+  st->settle_data = data;
+  st->settle_hook = settle_hook;
+}
+
+void
+kick_settle_timer(struct settle_timer *st)
+{
+  ASSUME(st != NULL);
+
+  st->base_settle_time = current_time();
+
+  timer *t = st->t;
+  if (!tm_active(t))
+    tm_set(t, settled_time(st));
+}
index 4a3a210834f6729df527b2089749985677e46e19..f952bc01977a247f45b2f1f2d05fcb1cbd166858 100644 (file)
@@ -134,4 +134,22 @@ btime tm_parse_time(const char *x);
 void tm_format_time(char *x, struct timeformat *fmt, btime t);
 int tm_format_real_time(char *x, size_t max, const char *fmt, btime t);
 
+/*
+ *   Settle timer
+ */
+
+struct settle_timer {
+  btime min_settle_time;
+  btime max_settle_time;
+  btime base_settle_time;
+  btime last_change;
+  timer *t;
+  void (*settle_hook)(struct settle_timer *t);
+  void *settle_data;
+};
+
+void stm_init(struct settle_timer *st, pool *p, void *data, void (*settle_hook)(struct settle_timer *st));
+void kick_settle_timer(struct settle_timer *st);
+void settle_timer_changed(struct settle_timer *st);
+
 #endif
index 54aa16468d6868b685cadabd209932daee8df66d..a784838232aecaa3e812be0ea154e34cfbb9a6a7 100644 (file)
@@ -1031,6 +1031,10 @@ channel_reconfigure(struct channel *c, struct channel_config *cf)
     }
   }
 
+  /* update pointers from config to channel and vice versa */
+  cf->channel = c;
+  c->config = cf;
+
   /* Execute channel-specific reconfigure hook */
   if (c->class->reconfigure && !c->class->reconfigure(c, cf, &import_changed, &export_changed))
     return 0;
index 60b808307d6e0d49365999c49d79bfed4aabfc13..6bb57bfa86561ac81212cdc0bafb6836375a603e 100644 (file)
@@ -93,7 +93,8 @@ void protos_dump_all(void);
 extern struct protocol
   proto_device, proto_radv, proto_rip, proto_static, proto_mrt,
   proto_ospf, proto_perf,
-  proto_pipe, proto_bgp, proto_bfd, proto_babel, proto_rpki;
+  proto_pipe, proto_bgp, proto_bfd, proto_babel, proto_rpki,
+  proto_stats;
 
 /*
  *     Routing Protocol Instance
@@ -492,6 +493,7 @@ struct channel_class {
 };
 
 extern struct channel_class channel_bgp;
+extern struct channel_class channel_stats;
 
 struct channel_config {
   node n;
index ef573d2afa64772eae8fdbea75fbec81d4809b93..f5eb5b35e5ef2d5838c28cd41dc7081f2d2f9ee3 100644 (file)
--- a/proto/Doc
+++ b/proto/Doc
@@ -7,5 +7,6 @@ C pipe
 C radv
 C rip
 C rpki
+C stats
 C static
 S ../nest/rt-dev.c
diff --git a/proto/stats/Doc b/proto/stats/Doc
new file mode 100644 (file)
index 0000000..cb89099
--- /dev/null
@@ -0,0 +1 @@
+S stats.c
diff --git a/proto/stats/Makefile b/proto/stats/Makefile
new file mode 100644 (file)
index 0000000..19e972c
--- /dev/null
@@ -0,0 +1,7 @@
+src := stats.c
+obj := $(src-o-files)
+$(all-daemon)
+$(cf-local)
+$(call proto-build,stats_build)
+
+tests_objs := $(tests_objs) $(src-o-files)
diff --git a/proto/stats/config.Y b/proto/stats/config.Y
new file mode 100644 (file)
index 0000000..38b7028
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ *     BIRD -- Statistics Protocol Configuration
+ *
+ *      (c) 2022 Vojtech Vilimek <vojtech.vilimek@nic.cz>
+ *      (c) 2022 CZ.NIC z.s.p.o. 
+ *
+ *     Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+CF_HDR
+
+#include "proto/stats/stats.h"
+
+CF_DEFINES
+
+#define STATS_CFG ((struct stats_config *) this_proto)
+#define STATS_CC ((struct stats_channel_config *) this_channel)
+
+CF_DECLS
+
+CF_KEYWORDS(STATS, TABLE, MAX, MIN, SETTLE, TIME)
+
+%type <cc> stats_channel_start
+
+CF_GRAMMAR
+
+proto: stats_proto '}' { this_channel = NULL; }  ;
+
+stats_proto_start: proto_start STATS
+{
+  this_proto = proto_config_new(&proto_stats, $1);
+  init_list(&STATS_CFG->terms);
+}
+
+proto_name ;
+
+stats_channel_opt_list:
+    /* empty */
+  | '{' stats_opts '}'
+  ;
+
+stats_opt:
+    type symbol '=' term {
+      
+      struct stats_term_config *tc = cfg_alloc(sizeof(struct stats_term_config));
+      struct f_val *val = cfg_allocz(sizeof(struct stats_term_config));
+      tc->code = (const struct f_line *) f_linearize($4);
+      tc->type = $1;
+      tc->name = $2->name;
+      tc->val = val;
+     
+      /* greater then F_RETURN, therefore 2 */
+      //if (f_eval(f_linearize($4), &val) > 2) cf_error("Runtime error");
+      //if (val.type != $1) cf_error("The expresion does not match defined type");
+
+      add_tail(&STATS_CFG->terms, (node *) tc);
+      stats_eval_term(tc);
+
+      $2 = cf_define_symbol($2, SYM_COUNTER_TERM, val, val);
+    }
+  | MIN SETTLE TIME expr_us { STATS_CC->min_settle_time = $4; }
+  | MAX SETTLE TIME expr_us { STATS_CC->max_settle_time = $4; }
+  ;
+
+stats_opts:
+    /* empty */
+  | stats_opts channel_item ';'
+  | stats_opts stats_opt ';'
+  ;
+     
+stats_proto_channel: stats_channel_start stats_channel_opt_list channel_end ;
+
+stats_channel_start: net_type symbol
+{
+  this_channel = channel_config_get(&channel_stats, $2->name, $1, this_proto);
+  STATS_CC->min_settle_time = 1 S_;
+  STATS_CC->max_settle_time = 20 S_;
+  $2 = cf_define_symbol($2, SYM_COUNTER, ch_config, this_channel);
+}
+
+stats_proto:
+   stats_proto_start '{'
+ | stats_proto proto_item ';'
+ | stats_proto stats_proto_channel ';'
+ | stats_proto  ';'
+ ;
+
+CF_CODE
+
+CF_END
diff --git a/proto/stats/stats-pub.h b/proto/stats/stats-pub.h
new file mode 100644 (file)
index 0000000..9e1bc5b
--- /dev/null
@@ -0,0 +1,5 @@
+#ifndef _BIRD_STATS_PUB_H_
+#define _BIRD_STATS_PUB_H_
+
+extern int stats_get_counter(struct symbol *sym);
+#endif
diff --git a/proto/stats/stats.c b/proto/stats/stats.c
new file mode 100644 (file)
index 0000000..98462a9
--- /dev/null
@@ -0,0 +1,280 @@
+/*
+ *     BIRD -- Statistics Protocol
+ *
+ *      (c) 2022       Vojtech Vilimek <vojtech.vilimek@nic.cz>
+ *      (c) 2022       CZ.NIC z.s.p.o.
+ *
+ *     Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+/**
+ * DOC: Stats
+ *
+ */
+
+#undef LOCAL_DEBUG
+
+#include "nest/bird.h"
+#include "nest/iface.h"
+#include "nest/protocol.h"
+#include "nest/rt.h"
+#include "nest/cli.h"
+#include "conf/conf.h"
+#include "filter/filter.h"
+#include "lib/string.h"
+#include "lib/timer.h"
+
+#include "stats.h"
+
+static void stats_settle_timer(struct settle_timer *st);
+
+static void
+stats_rt_notify(struct proto *P UNUSED, struct channel *src_ch, const net_addr *n UNUSED, rte *new, const rte *old)
+{
+  struct stats_channel *ch = (void *) src_ch;
+
+  int changed = 0;
+  if (new && old)
+    /* count of exported routes stays the same */
+    ;
+  else if (!old)
+  {
+    ch->_counter++;
+    changed = 1;
+  }
+  else if (!new)
+  {
+    ch->_counter--;
+    changed = 1;
+  }
+  else  /* shouldn't happen */
+  {
+    bug("Both pointers *new and *old in rt_notify are NULL");
+  }
+
+  if (changed)
+  {
+    settle_timer_changed(&ch->settle_timer);
+    kick_settle_timer(&ch->settle_timer);
+  }
+}
+
+static void
+stats_reload_routes(struct channel *C)
+{
+  // TODO
+  struct stats_channel *c = (void *) C;
+
+  c->_counter = c->counter = 0;
+  channel_request_feeding(C);
+}
+
+static struct proto *
+stats_init(struct proto_config *CF)
+{
+  struct proto *P = proto_new(CF);
+  struct stats_proto *p = (void *) P;
+
+  P->rt_notify = stats_rt_notify;
+  P->reload_routes = stats_reload_routes;
+
+  p->rl_gen = (struct tbf) TBF_DEFAULT_LOG_LIMITS;
+
+  return P;
+}
+
+static void 
+stats_configure_channels(struct proto *P, struct proto_config *CF)
+{
+  struct channel_config *cc;
+  WALK_LIST(cc, CF->channels)
+  {
+    struct channel *c = proto_find_channel_by_name(P, cc->name);
+    proto_configure_channel(P, &c, cc);
+  } 
+}
+
+static int
+stats_start(struct proto *P)
+{
+  stats_configure_channels(P, P->cf);
+
+  /* evaluate terms on protocol start */
+  struct stats_term_config *tc;
+  WALK_LIST(tc, ((struct stats_config *) P->cf)->terms)
+  {
+    stats_eval_term(tc);
+  }
+
+  return PS_UP;
+}
+
+static int
+stats_reconfigure(struct proto *P, struct proto_config *CF)
+{
+  struct stats_proto *p = (void *) P;
+  struct stats_config *new = (void *) CF;
+
+  struct channel *c;
+  WALK_LIST(c, p->p.channels)
+    c->stale = 1;
+
+  struct channel_config *cc;
+  WALK_LIST(cc, new->c.channels)
+  {
+    c = proto_find_channel_by_name(P, cc->name);
+    if (!proto_configure_channel(P, &c, cc))
+      return 0;
+
+    if (c)
+    {
+      struct stats_channel *sc = (void *) c;
+      struct stats_channel_config *scc = (void *) cc;
+
+      sc->settle_timer.min_settle_time = scc->min_settle_time;
+      sc->settle_timer.max_settle_time = scc->max_settle_time;
+
+      if (sc->counter != sc->_counter)
+      {
+       sc->counter = sc->_counter;
+
+       /* notify all hooked filters */
+       // TODO here
+      }
+
+      c->stale = 0;
+    }
+  }
+
+  struct channel *c2;
+  WALK_LIST_DELSAFE(c, c2, p->p.channels)
+    if (c->stale && !proto_configure_channel(P, &c, NULL))
+      return 0;
+  
+  return 1;
+}
+
+static void
+stats_show_proto_info(struct proto *P)
+{
+  struct stats_proto *p = (void *) P;
+
+  struct stats_channel *sc;
+  WALK_LIST(sc, p->p.channels)
+  {
+    cli_msg(-1006, "  Channel %s", sc->c.name);
+    cli_msg(-1006, "    Exports:  %10u (currently:  %10u)",
+             sc->counter,
+             sc->_counter);
+    if (!P->disabled)
+    {
+      cli_msg(-1006, "    Settle time:  %4u s", sc->settle_timer.min_settle_time TO_S);
+      cli_msg(-1006, "    Settle time:  %4u s", sc->settle_timer.max_settle_time TO_S);
+    }
+  }
+
+  cli_msg(-1006, "  Terms:");
+
+  struct stats_term_config *tc;
+  WALK_LIST(tc, ((struct stats_config *) P->cf)->terms)
+  {
+    stats_eval_term(tc);
+    cli_msg(-1006, "    %s = %s", tc->name, val_dump(tc->val));
+  }
+}
+
+void
+stats_update_debug(struct proto *P)
+{
+  struct channel *c;
+  WALK_LIST(c, P->channels)
+  {
+    c->debug = P->debug;
+  }
+}
+
+static void
+stats_settle_timer(struct settle_timer *st)
+{
+  struct stats_channel *c = st->settle_data;
+
+  /* update only if real change happen */
+  if (c->counter != c->_counter)
+  {
+    c->counter = c->_counter;
+    /* do update here */
+    // WALK_LIST(s, subscribers)
+    // { ... }
+  }
+}
+
+static int
+stats_channel_start(struct channel *C)
+{
+  struct stats_channel *c = (void *) C;
+  struct stats_channel_config *cc = (void *) C->config;
+  struct stats_proto *p = (void *) C->proto;
+
+  c->pool = p->p.pool;
+
+  stm_init(&c->settle_timer, c->pool, (void *)c, stats_settle_timer);
+
+  c->settle_timer.min_settle_time = cc->min_settle_time;
+  c->settle_timer.max_settle_time = cc->max_settle_time;
+
+  c->_counter = 0;
+  c->counter = 0;
+
+  return 0;
+}
+
+static void
+stats_channel_shutdown(struct channel *C)
+{
+  struct stats_channel *c = (void *) C;
+
+  tm_stop(c->settle_timer.t);
+
+  c->_counter = 0;
+  c->counter = 0;
+  c->pool = NULL;
+}
+
+int
+stats_get_counter(struct symbol *sym)
+{
+  if (sym->ch_config->channel)
+    return (int) ((struct stats_channel *) sym->ch_config->channel)->counter;
+  else
+    return 0;
+}
+
+void stats_eval_term(struct stats_term_config *tc)
+{
+  f_eval(tc->code, tc->val);
+}
+
+struct channel_class channel_stats = {
+  .channel_size =      sizeof(struct stats_channel),
+  .config_size =       sizeof(struct stats_channel_config),
+  .start =             stats_channel_start,
+  .shutdown =          stats_channel_shutdown,
+};
+
+struct protocol proto_stats = {
+  .name =              "Stats",
+  .template =          "stat%d",
+  .channel_mask =      NB_ANY,
+  .proto_size =                sizeof(struct stats_proto),
+  .config_size =       sizeof(struct stats_config),
+  .init =              stats_init,
+  .start =             stats_start,
+  .reconfigure =       stats_reconfigure,
+  .show_proto_info =   stats_show_proto_info
+};
+
+void
+stats_build(void)
+{
+  proto_build(&proto_stats);
+}
diff --git a/proto/stats/stats.h b/proto/stats/stats.h
new file mode 100644 (file)
index 0000000..2c5d725
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ *     BIRD -- Statistics Protocol
+ *
+ *      (c) 2022 Vojtech Vilimek <vojtech.vilimek@nic.cz>
+ *      (c) 2022 CZ.NIC z.s.p.o.
+ *
+ *     Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+#ifndef _BIRD_STATS_H_
+#define _BIRD_STATS_H_
+#include "lib/timer.h"
+#include "filter/data.h"
+
+struct stats_channel;
+
+struct stats_term_config {
+  node n;
+  const struct f_line *code;
+  struct f_val *val;
+  int type;                     /* type declared in configuration */
+  const char *name;
+};
+
+struct stats_config {
+  struct proto_config c;
+  list terms;                    /* list of counter terms */
+};
+
+struct stats_proto {
+  struct proto p;
+  struct stats_channel *c;
+  struct tbf rl_gen;
+};
+
+struct stats_channel {
+  struct channel c;
+  pool *pool;                     /* copy of procotol pool */
+  u32 _counter;                          /* internal counter */
+  u32 counter;                   /* publicly accessible counter */
+  struct settle_timer settle_timer;
+};
+
+struct stats_channel_config {
+  struct channel_config c;
+  btime min_settle_time;              /* wait before notifying filters */
+  btime max_settle_time;
+};
+
+int stats_get_counter(struct symbol *sym);
+void stats_eval_term(struct stats_term_config *tc);
+
+#endif