]> git.ipfire.org Git - thirdparty/collectd.git/commitdiff
common: Add support for floating point counters …
authorFlorian Forster <octo@collectd.org>
Sat, 3 Feb 2024 19:52:39 +0000 (20:52 +0100)
committerFlorian Forster <octo@collectd.org>
Sat, 3 Feb 2024 19:52:39 +0000 (20:52 +0100)
… to `rate_to_value()` and `value_to_rate()`.

Makefile.am
src/utils/common/common.c
src/utils/common/common.h
src/utils/common/common_test.c

index 62919d5219637445fad051ae23149d9169dcddcb..8f39d989a9f25a200a59f8e16b08cf7cd0ea0521 100644 (file)
@@ -422,7 +422,7 @@ libavltree_la_SOURCES = \
 libcommon_la_SOURCES = \
        src/utils/common/common.c \
        src/utils/common/common.h
-libcommon_la_LIBADD = libmetric.la libstrbuf.la $(COMMON_LIBS)
+libcommon_la_LIBADD = libmetric.la libstrbuf.la -lm $(COMMON_LIBS)
 
 libheap_la_SOURCES = \
        src/utils/heap/heap.c \
index 6fe7427658038c10f607f0e196b3e7a4c7f2d65b..5ad0328f1f1a330319f0fb08707b93255eae9301 100644 (file)
@@ -935,38 +935,42 @@ int format_name(char *ret, int ret_len, const char *hostname,
 int format_values(strbuf_t *buf, metric_t const *m, bool store_rates) {
   strbuf_printf(buf, "%.3f", CDTIME_T_TO_DOUBLE(m->time));
 
-  if (m->family->type == METRIC_TYPE_GAUGE) {
-    /* Solaris' printf tends to print NAN as "-nan", breaking unit tests, so we
-     * introduce a special case here. */
-    if (isnan(m->value.gauge)) {
-      strbuf_print(buf, ":nan");
-    } else {
-      strbuf_printf(buf, ":" GAUGE_FORMAT, m->value.gauge);
-    }
-  } else if (store_rates) {
+  if (store_rates) {
     gauge_t rates = NAN;
-    int status = uc_get_rate(m, &rates);
-    if (status != 0) {
-      WARNING("format_values: uc_get_rate failed.");
-      return status;
+    int err = uc_get_rate(m, &rates);
+    if (err) {
+      ERROR("format_values: uc_get_rate failed: %s", STRERROR(err));
+      return err;
     }
     if (isnan(rates)) {
       strbuf_print(buf, ":nan");
     } else {
       strbuf_printf(buf, ":" GAUGE_FORMAT, rates);
     }
-  } else if (m->family->type == METRIC_TYPE_COUNTER) {
-    strbuf_printf(buf, ":%" PRIu64, m->value.counter);
-  } else if (m->family->type == METRIC_TYPE_FPCOUNTER) {
-    strbuf_printf(buf, ":" GAUGE_FORMAT, m->value.fpcounter);
-  } else if (m->family->type == DS_TYPE_DERIVE) {
-    strbuf_printf(buf, ":%" PRIi64, m->value.derive);
-  } else {
-    ERROR("format_values: Unknown metric type: %d", m->family->type);
-    return -1;
+    return 0;
   }
 
-  return 0;
+  if (m->family->type == DS_TYPE_DERIVE) {
+    return strbuf_printf(buf, ":%" PRIi64, m->value.derive);
+  }
+
+  switch (m->family->type) {
+  case METRIC_TYPE_GAUGE:
+    /* Solaris' printf tends to print NAN as "-nan", breaking unit tests, so we
+     * introduce a special case here. */
+    if (isnan(m->value.gauge)) {
+      return strbuf_print(buf, ":nan");
+    }
+    return strbuf_printf(buf, ":" GAUGE_FORMAT, m->value.gauge);
+  case METRIC_TYPE_COUNTER:
+    return strbuf_printf(buf, ":%" PRIu64, m->value.counter);
+  case METRIC_TYPE_FPCOUNTER:
+    return strbuf_printf(buf, ":" GAUGE_FORMAT, m->value.fpcounter);
+  case METRIC_TYPE_UNTYPED:
+    break;
+  }
+  ERROR("format_values: Unknown metric type: %d", m->family->type);
+  return EINVAL;
 } /* }}} int format_values */
 
 int parse_value(const char *value_orig, value_t *ret_value, int ds_type) {
@@ -1221,11 +1225,12 @@ counter_t counter_diff(counter_t old_value, counter_t new_value) {
 } /* counter_t counter_diff */
 
 int rate_to_value(value_t *ret_value, gauge_t rate, /* {{{ */
-                  rate_to_value_state_t *state, int ds_type, cdtime_t t) {
+                  rate_to_value_state_t *state, metric_type_t type,
+                  cdtime_t t) {
   gauge_t delta_gauge;
   cdtime_t delta_t;
 
-  if (ds_type == DS_TYPE_GAUGE) {
+  if (type == METRIC_TYPE_GAUGE) {
     state->last_value.gauge = rate;
     state->last_time = t;
 
@@ -1235,7 +1240,8 @@ int rate_to_value(value_t *ret_value, gauge_t rate, /* {{{ */
 
   /* Counter can't handle negative rates. Reset "last time" to zero, so that
    * the next valid rate will re-initialize the structure. */
-  if ((rate < 0.0) && (ds_type == DS_TYPE_COUNTER)) {
+  if ((rate < 0.0) &&
+      (type == METRIC_TYPE_COUNTER || type == METRIC_TYPE_FPCOUNTER)) {
     memset(state, 0, sizeof(*state));
     return EINVAL;
   }
@@ -1252,32 +1258,63 @@ int rate_to_value(value_t *ret_value, gauge_t rate, /* {{{ */
   /* Previous value is invalid. */
   if (state->last_time == 0) /* {{{ */
   {
-    if (ds_type == DS_TYPE_DERIVE) {
-      state->last_value.derive = (derive_t)rate;
+    if (type == DS_TYPE_DERIVE) {
+      state->last_value.derive = (derive_t)floor(rate);
       state->residual = rate - ((gauge_t)state->last_value.derive);
-    } else if (ds_type == DS_TYPE_COUNTER) {
+      state->last_time = t;
+      return EAGAIN;
+    }
+
+    switch (type) {
+    case METRIC_TYPE_GAUGE:
+      /* not reached */
+      break;
+    case METRIC_TYPE_COUNTER:
       state->last_value.counter = (counter_t)rate;
       state->residual = rate - ((gauge_t)state->last_value.counter);
-    } else {
-      assert(23 == 42);
+      break;
+    case METRIC_TYPE_FPCOUNTER:
+      state->last_value.fpcounter = (fpcounter_t)rate;
+      state->residual = 0;
+      break;
+    case METRIC_TYPE_UNTYPED:
+      ERROR("rate_to_value: invalid metric type: %d", type);
+      return EINVAL;
     }
 
     state->last_time = t;
     return EAGAIN;
   } /* }}} */
 
-  if (ds_type == DS_TYPE_DERIVE) {
-    derive_t delta_derive = (derive_t)delta_gauge;
-
+  if (type == DS_TYPE_DERIVE) {
+    derive_t delta_derive = (derive_t)floor(delta_gauge);
     state->last_value.derive += delta_derive;
     state->residual = delta_gauge - ((gauge_t)delta_derive);
-  } else if (ds_type == DS_TYPE_COUNTER) {
-    counter_t delta_counter = (counter_t)delta_gauge;
 
+    state->last_time = t;
+    *ret_value = state->last_value;
+    return 0;
+  }
+
+  switch (type) {
+  case METRIC_TYPE_GAUGE:
+    /* not reached */
+    break;
+  case METRIC_TYPE_COUNTER: {
+    counter_t delta_counter = (counter_t)delta_gauge;
     state->last_value.counter += delta_counter;
     state->residual = delta_gauge - ((gauge_t)delta_counter);
-  } else {
-    assert(23 == 42);
+    break;
+  }
+  case METRIC_TYPE_FPCOUNTER: {
+    fpcounter_t delta_counter = (fpcounter_t)delta_gauge;
+    state->last_value.fpcounter += delta_counter;
+    state->residual = 0;
+    break;
+  }
+  case METRIC_TYPE_UNTYPED:
+    ERROR("rate_to_value: invalid metric type: %d", type);
+    return EINVAL;
   }
 
   state->last_time = t;
@@ -1285,19 +1322,49 @@ int rate_to_value(value_t *ret_value, gauge_t rate, /* {{{ */
   return 0;
 } /* }}} value_t rate_to_value */
 
+static int calculate_rate(gauge_t *ret_rate, value_t value, metric_type_t type,
+                          gauge_t interval, value_to_rate_state_t *state) {
+  if (type == DS_TYPE_DERIVE) {
+    derive_t diff = value.derive - state->last_value.derive;
+    *ret_rate = ((gauge_t)diff) / ((gauge_t)interval);
+    return 0;
+  }
+
+  switch (type) {
+  case METRIC_TYPE_GAUGE: {
+    *ret_rate = value.gauge;
+    return 0;
+  }
+  case METRIC_TYPE_COUNTER: {
+    counter_t diff = counter_diff(state->last_value.counter, value.counter);
+    *ret_rate = ((gauge_t)diff) / ((gauge_t)interval);
+    return 0;
+  }
+  case METRIC_TYPE_FPCOUNTER: {
+    if (state->last_value.fpcounter > value.fpcounter) {
+      *ret_rate = NAN;
+      return EAGAIN;
+    }
+    fpcounter_t diff = value.fpcounter - state->last_value.fpcounter;
+    *ret_rate = ((gauge_t)diff) / ((gauge_t)interval);
+    return 0;
+  }
+  case METRIC_TYPE_UNTYPED:
+    break;
+  }
+  ERROR("value_to_rate: invalid metric type: %d", type);
+  return EINVAL;
+}
+
 int value_to_rate(gauge_t *ret_rate, /* {{{ */
-                  value_t value, int ds_type, cdtime_t t,
+                  value_t value, metric_type_t type, cdtime_t t,
                   value_to_rate_state_t *state) {
-  gauge_t interval;
-
   /* Another invalid state: The time is not increasing. */
   if (t <= state->last_time) {
     memset(state, 0, sizeof(*state));
     return EINVAL;
   }
 
-  interval = CDTIME_T_TO_DOUBLE(t - state->last_time);
-
   /* Previous value is invalid. */
   if (state->last_time == 0) {
     state->last_value = value;
@@ -1305,28 +1372,15 @@ int value_to_rate(gauge_t *ret_rate, /* {{{ */
     return EAGAIN;
   }
 
-  switch (ds_type) {
-  case DS_TYPE_DERIVE: {
-    derive_t diff = value.derive - state->last_value.derive;
-    *ret_rate = ((gauge_t)diff) / ((gauge_t)interval);
-    break;
-  }
-  case DS_TYPE_GAUGE: {
-    *ret_rate = value.gauge;
-    break;
-  }
-  case DS_TYPE_COUNTER: {
-    counter_t diff = counter_diff(state->last_value.counter, value.counter);
-    *ret_rate = ((gauge_t)diff) / ((gauge_t)interval);
-    break;
-  }
-  default:
-    return EINVAL;
+  gauge_t interval = CDTIME_T_TO_DOUBLE(t - state->last_time);
+  int err = calculate_rate(ret_rate, value, type, interval, state);
+  if (err && err != EAGAIN) {
+    return err;
   }
 
   state->last_value = value;
   state->last_time = t;
-  return 0;
+  return err;
 } /* }}} value_t rate_to_value */
 
 int service_name_to_port_number(const char *service_name) {
index 069e1b6797b1929f20d5e82293b96494fdba318e..a8899343eb5e0f5ea2cbf48396b53af271db5134 100644 (file)
@@ -386,10 +386,10 @@ counter_t counter_diff(counter_t old_value, counter_t new_value);
  * this case the value_t is invalid and the next call should succeed. Other
  * return values indicate an error. */
 int rate_to_value(value_t *ret_value, gauge_t rate,
-                  rate_to_value_state_t *state, int ds_type, cdtime_t t);
+                  rate_to_value_state_t *state, metric_type_t type, cdtime_t t);
 
-int value_to_rate(gauge_t *ret_rate, value_t value, int ds_type, cdtime_t t,
-                  value_to_rate_state_t *state);
+int value_to_rate(gauge_t *ret_rate, value_t value, metric_type_t type,
+                  cdtime_t t, value_to_rate_state_t *state);
 
 /* Converts a service name (a string) to a port number
  * (in the range [1-65535]). Returns less than zero on error. */
index 58a11b2b9850257c84cae4731ce4ed3240b1069b..80ba765712a58cfa381d38001608905a286ed4aa 100644 (file)
@@ -299,53 +299,285 @@ DEF_TEST(strunescape) {
   return 0;
 }
 
+DEF_TEST(rate_to_value) {
+  struct {
+    char *name;
+    gauge_t rate;
+    rate_to_value_state_t state;
+    metric_type_t type;
+    cdtime_t time;
+    value_t want;
+    gauge_t want_residual;
+    int want_err;
+  } cases[] = {
+      {
+          .name = "zero value",
+          .rate = 1.,
+          .state = {.last_time = 0},
+          .type = METRIC_TYPE_COUNTER,
+          .time = TIME_T_TO_CDTIME_T(10),
+          .want_err = EAGAIN,
+      },
+      {
+          .name = "counter",
+          .rate = 1.,
+          .state =
+              {
+                  .last_value = {.counter = 1000},
+                  .last_time = TIME_T_TO_CDTIME_T(10),
+                  .residual = 0,
+              },
+          .type = METRIC_TYPE_COUNTER,
+          .time = TIME_T_TO_CDTIME_T(20),
+          .want = {.counter = 1010},
+      },
+      {
+          .name = "residual gets rounded down",
+          .rate = 0.999,
+          .state =
+              {
+                  .last_value = {.counter = 1000},
+                  .last_time = TIME_T_TO_CDTIME_T(10),
+                  .residual = 0,
+              },
+          .type = METRIC_TYPE_COUNTER,
+          .time = TIME_T_TO_CDTIME_T(20),
+          .want = {.counter = 1009},
+          .want_residual = 0.99,
+      },
+      {
+          .name = "residual gets added to result",
+          .rate = 0.0011,
+          .state =
+              {
+                  .last_value = {.counter = 1000},
+                  .last_time = TIME_T_TO_CDTIME_T(10),
+                  .residual = 0.99,
+              },
+          .type = METRIC_TYPE_COUNTER,
+          .time = TIME_T_TO_CDTIME_T(20),
+          .want = {.counter = 1001},
+          .want_residual = 0.001,
+      },
+      {
+          .name = "fpcounter",
+          .rate = 1.234,
+          .state =
+              {
+                  .last_value = {.fpcounter = 1000},
+                  .last_time = TIME_T_TO_CDTIME_T(10),
+                  .residual = 0,
+              },
+          .type = METRIC_TYPE_FPCOUNTER,
+          .time = TIME_T_TO_CDTIME_T(20),
+          .want = {.fpcounter = 1012.34},
+      },
+      {
+          .name = "derive",
+          .rate = 1.,
+          .state =
+              {
+                  .last_value = {.derive = 1000},
+                  .last_time = TIME_T_TO_CDTIME_T(10),
+                  .residual = 0,
+              },
+          .type = DS_TYPE_DERIVE,
+          .time = TIME_T_TO_CDTIME_T(20),
+          .want = {.derive = 1010},
+      },
+      {
+          .name = "derive initialization with negative rate",
+          .rate = 1.05,
+          .state = {.last_time = 0},
+          .type = DS_TYPE_DERIVE,
+          .time = TIME_T_TO_CDTIME_T(20),
+          .want_err = EAGAIN,
+          .want_residual = .5,
+      },
+      {
+          .name = "derive with negative rate",
+          .rate = -1.,
+          .state =
+              {
+                  .last_value = {.derive = 1000},
+                  .last_time = TIME_T_TO_CDTIME_T(10),
+                  .residual = 0,
+              },
+          .type = DS_TYPE_DERIVE,
+          .time = TIME_T_TO_CDTIME_T(20),
+          .want = {.derive = 990},
+      },
+      {
+          .name = "residual gets rounded down",
+          .rate = -1.01,
+          .state =
+              {
+                  .last_value = {.derive = 1000},
+                  .last_time = TIME_T_TO_CDTIME_T(10),
+                  .residual = 0,
+              },
+          .type = DS_TYPE_DERIVE,
+          .time = TIME_T_TO_CDTIME_T(20),
+          .want = {.derive = 989},
+          .want_residual = .9,
+      },
+  };
+
+  for (size_t i = 0; i < STATIC_ARRAY_SIZE(cases); i++) {
+    printf("## Case %zu %s\n", i, cases[i].name);
+
+    rate_to_value_state_t state = cases[i].state;
+    value_t got = {0};
+    EXPECT_EQ_INT(cases[i].want_err,
+                  rate_to_value(&got, cases[i].rate, &state, cases[i].type,
+                                cases[i].time));
+    if (cases[i].want_err) {
+      continue;
+    }
+
+    switch (cases[i].type) {
+    case METRIC_TYPE_COUNTER:
+      EXPECT_EQ_UINT64(cases[i].want.counter, got.counter);
+      EXPECT_EQ_UINT64(cases[i].want.counter, state.last_value.counter);
+      break;
+    case METRIC_TYPE_GAUGE:
+      EXPECT_EQ_DOUBLE(cases[i].want.gauge, got.gauge);
+      EXPECT_EQ_DOUBLE(cases[i].want.gauge, state.last_value.gauge);
+      break;
+    case METRIC_TYPE_FPCOUNTER:
+      EXPECT_EQ_DOUBLE(cases[i].want.fpcounter, got.fpcounter);
+      EXPECT_EQ_UINT64(cases[i].want.fpcounter, state.last_value.fpcounter);
+      break;
+    case METRIC_TYPE_UNTYPED:
+      LOG(false, "invalid metric type");
+      break;
+    }
+
+    EXPECT_EQ_UINT64(cases[i].time, state.last_time);
+    EXPECT_EQ_DOUBLE(cases[i].want_residual, state.residual);
+  }
+
+  return 0;
+}
+
 DEF_TEST(value_to_rate) {
   struct {
+    char *name;
     time_t t0;
     time_t t1;
-    int ds_type;
+    metric_type_t type;
     value_t v0;
     value_t v1;
     gauge_t want;
+    int want_err;
   } cases[] = {
-      {0, 10, DS_TYPE_DERIVE, {.derive = 0}, {.derive = 1000}, NAN},
-      {10, 20, DS_TYPE_DERIVE, {.derive = 1000}, {.derive = 2000}, 100.0},
-      {20, 30, DS_TYPE_DERIVE, {.derive = 2000}, {.derive = 1800}, -20.0},
-      {0, 10, DS_TYPE_COUNTER, {.counter = 0}, {.counter = 1000}, NAN},
-      {10, 20, DS_TYPE_COUNTER, {.counter = 1000}, {.counter = 5000}, 400.0},
-      /* 32bit wrap-around. */
-      {20,
-       30,
-       DS_TYPE_COUNTER,
-       {.counter = 4294967238ULL},
-       {.counter = 42},
-       10.0},
-      /* 64bit wrap-around. */
-      {30,
-       40,
-       DS_TYPE_COUNTER,
-       {.counter = 18446744073709551558ULL},
-       {.counter = 42},
-       10.0},
+      {
+          .name = "derive_t init",
+          .t0 = 0,
+          .t1 = 10,
+          .type = DS_TYPE_DERIVE,
+          .v0 = {.derive = 0},
+          .v1 = {.derive = 1000},
+          .want_err = EAGAIN,
+      },
+      {
+          .name = "derive_t increase",
+          .t0 = 10,
+          .t1 = 20,
+          .type = DS_TYPE_DERIVE,
+          .v0 = {.derive = 1000},
+          .v1 = {.derive = 2000},
+          .want = 100.0,
+      },
+      {
+          .name = "derive_t decrease",
+          .t0 = 20,
+          .t1 = 30,
+          .type = DS_TYPE_DERIVE,
+          .v0 = {.derive = 2000},
+          .v1 = {.derive = 1800},
+          .want = -20.0,
+      },
+      {
+          .name = "counter_t init",
+          .t0 = 0,
+          .t1 = 10,
+          .type = METRIC_TYPE_COUNTER,
+          .v0 = {.counter = 0},
+          .v1 = {.counter = 1000},
+          .want_err = EAGAIN,
+      },
+      {
+          .name = "counter_t increase",
+          .t0 = 10,
+          .t1 = 20,
+          .type = METRIC_TYPE_COUNTER,
+          .v0 = {.counter = 1000},
+          .v1 = {.counter = 5000},
+          .want = 400.0,
+      },
+      {
+          .name = "counter_t 32bit wrap-around",
+          .t0 = 20,
+          .t1 = 30,
+          .type = METRIC_TYPE_COUNTER,
+          .v0 = {.counter = 4294967238ULL},
+          .v1 = {.counter = 42},
+          .want = 10.0,
+      },
+      {
+          .name = "counter_t 64bit wrap-around",
+          .t0 = 30,
+          .t1 = 40,
+          .type = METRIC_TYPE_COUNTER,
+          .v0 = {.counter = 18446744073709551558ULL},
+          .v1 = {.counter = 42},
+          .want = 10.0,
+      },
+      {
+          .name = "fpcounter_t init",
+          .t0 = 0,
+          .t1 = 10,
+          .type = METRIC_TYPE_FPCOUNTER,
+          .v0 = {.fpcounter = 0.},
+          .v1 = {.fpcounter = 10.},
+          .want_err = EAGAIN,
+      },
+      {
+          .name = "fpcounter_t increase",
+          .t0 = 10,
+          .t1 = 20,
+          .type = METRIC_TYPE_FPCOUNTER,
+          .v0 = {.fpcounter = 10.},
+          .v1 = {.fpcounter = 50.5},
+          .want = (50.5 - 10.) / (20. - 10.),
+      },
+      {
+          .name = "fpcounter_t reset",
+          .t0 = 20,
+          .t1 = 30,
+          .type = METRIC_TYPE_FPCOUNTER,
+          .v0 = {.fpcounter = 100.0},
+          .v1 = {.fpcounter = 20.0},
+          .want_err = EAGAIN,
+      },
   };
 
   for (size_t i = 0; i < STATIC_ARRAY_SIZE(cases); i++) {
+    printf("## Case %zu %s\n", i, cases[i].name);
+
     cdtime_t t0 = TIME_T_TO_CDTIME_T(cases[i].t0);
     value_to_rate_state_t state = {
         .last_value = cases[i].v0,
         .last_time = t0,
     };
-    gauge_t got;
-
-    if (cases[i].t0 == 0) {
-      EXPECT_EQ_INT(EAGAIN,
-                    value_to_rate(&got, cases[i].v1, cases[i].ds_type,
-                                  TIME_T_TO_CDTIME_T(cases[i].t1), &state));
+    gauge_t got = 0;
+    EXPECT_EQ_INT(cases[i].want_err,
+                  value_to_rate(&got, cases[i].v1, cases[i].type,
+                                TIME_T_TO_CDTIME_T(cases[i].t1), &state));
+    if (cases[i].want_err) {
       continue;
     }
-
-    EXPECT_EQ_INT(0, value_to_rate(&got, cases[i].v1, cases[i].ds_type,
-                                   TIME_T_TO_CDTIME_T(cases[i].t1), &state));
     EXPECT_EQ_DOUBLE(cases[i].want, got);
   }
 
@@ -415,6 +647,7 @@ int main(void) {
   RUN_TEST(escape_slashes);
   RUN_TEST(escape_string);
   RUN_TEST(strunescape);
+  RUN_TEST(rate_to_value);
   RUN_TEST(value_to_rate);
   RUN_TEST(format_values);
   RUN_TEST(string_has_suffix);