]> git.ipfire.org Git - thirdparty/collectd.git/commitdiff
format_json: Add support for appending metric_family_t's to the buffer.
authorFlorian Forster <octo@collectd.org>
Wed, 8 Jul 2020 20:18:04 +0000 (22:18 +0200)
committerFlorian Forster <octo@google.com>
Wed, 29 Jul 2020 11:40:54 +0000 (13:40 +0200)
With this change, multiple metric_family_t's can be added to a buffer
sequentially. The implementation that did not use libyajl has been
removed because it was unused (it only implemented marshalling of
value_list_t).

Makefile.am
src/utils/format_json/format_json.c
src/utils/format_json/format_json.h
src/utils/format_json/format_json_test.c

index fd9b7dd749db88c079ddfd6ee33ea50f526e5e2c..01c5bb773595d93cd1b668d28b96139ba7bb6954 100644 (file)
@@ -132,7 +132,6 @@ noinst_LTLIBRARIES = \
        libcmds.la \
        libcommon.la \
        libformat_graphite.la \
-       libformat_json.la \
        libheap.la \
        libidentity.la \
        libignorelist.la \
@@ -492,13 +491,14 @@ test_format_graphite_LDADD = \
        libstrbuf.la \
        -lm
 
+if BUILD_WITH_LIBYAJL
+noinst_LTLIBRARIES += libformat_json.la
 libformat_json_la_SOURCES = \
        src/utils/format_json/format_json.c \
        src/utils/format_json/format_json.h
 libformat_json_la_CPPFLAGS  = $(AM_CPPFLAGS)
 libformat_json_la_LDFLAGS   = $(AM_LDFLAGS)
 libformat_json_la_LIBADD    = libmetric.la
-if BUILD_WITH_LIBYAJL
 libformat_json_la_CPPFLAGS += $(BUILD_WITH_LIBYAJL_CPPFLAGS)
 libformat_json_la_LDFLAGS  += $(BUILD_WITH_LIBYAJL_LDFLAGS)
 libformat_json_la_LIBADD   += $(BUILD_WITH_LIBYAJL_LIBS)
index 23b95aabf5ae2af62bf422369bd388edb4738c48..5958b12d79d319f828b89c866e6574b94e8b365c 100644 (file)
 #include "utils/format_json/format_json.h"
 
 #include "plugin.h"
-#include "utils/avltree/avltree.h"
 #include "utils/common/common.h"
 #include "utils_cache.h"
 
-#if HAVE_LIBYAJL
 #include <yajl/yajl_common.h>
 #include <yajl/yajl_gen.h>
 #if HAVE_YAJL_YAJL_VERSION_H
 #if defined(YAJL_MAJOR) && (YAJL_MAJOR > 1)
 #define HAVE_YAJL_V2 1
 #endif
-#endif
-
-static int json_escape_string(char *buffer, size_t buffer_size, /* {{{ */
-                              const char *string) {
-  size_t dst_pos;
-
-  if ((buffer == NULL) || (string == NULL))
-    return -EINVAL;
-
-  if (buffer_size < 3)
-    return -ENOMEM;
-
-  dst_pos = 0;
-
-#define BUFFER_ADD(c)                                                          \
-  do {                                                                         \
-    if (dst_pos >= (buffer_size - 1)) {                                        \
-      buffer[buffer_size - 1] = '\0';                                          \
-      return -ENOMEM;                                                          \
-    }                                                                          \
-    buffer[dst_pos] = (c);                                                     \
-    dst_pos++;                                                                 \
-  } while (0)
-
-  /* Escape special characters */
-  BUFFER_ADD('"');
-  for (size_t src_pos = 0; string[src_pos] != 0; src_pos++) {
-    if ((string[src_pos] == '"') || (string[src_pos] == '\\')) {
-      BUFFER_ADD('\\');
-      BUFFER_ADD(string[src_pos]);
-    } else if (string[src_pos] <= 0x001F)
-      BUFFER_ADD('?');
-    else
-      BUFFER_ADD(string[src_pos]);
-  } /* for */
-  BUFFER_ADD('"');
-  buffer[dst_pos] = 0;
-
-#undef BUFFER_ADD
-
-  return 0;
-} /* }}} int json_escape_string */
-
-static int values_to_json(char *buffer, size_t buffer_size, /* {{{ */
-                          const data_set_t *ds, const value_list_t *vl,
-                          int store_rates) {
-  size_t offset = 0;
-  gauge_t *rates = NULL;
-
-  memset(buffer, 0, buffer_size);
-
-#define BUFFER_ADD(...)                                                        \
-  do {                                                                         \
-    int status;                                                                \
-    status = snprintf(buffer + offset, buffer_size - offset, __VA_ARGS__);     \
-    if (status < 1) {                                                          \
-      sfree(rates);                                                            \
-      return -1;                                                               \
-    } else if (((size_t)status) >= (buffer_size - offset)) {                   \
-      sfree(rates);                                                            \
-      return -ENOMEM;                                                          \
-    } else                                                                     \
-      offset += ((size_t)status);                                              \
-  } while (0)
-
-  BUFFER_ADD("[");
-  for (size_t i = 0; i < ds->ds_num; i++) {
-    if (i > 0)
-      BUFFER_ADD(",");
-
-    if (ds->ds[i].type == DS_TYPE_GAUGE) {
-      if (isfinite(vl->values[i].gauge))
-        BUFFER_ADD(JSON_GAUGE_FORMAT, vl->values[i].gauge);
-      else
-        BUFFER_ADD("null");
-    } else if (store_rates) {
-      if (rates == NULL)
-        rates = uc_get_rate_vl(ds, vl);
-      if (rates == NULL) {
-        WARNING("utils_format_json: uc_get_rate_vl failed.");
-        sfree(rates);
-        return -1;
-      }
-
-      if (isfinite(rates[i]))
-        BUFFER_ADD(JSON_GAUGE_FORMAT, rates[i]);
-      else
-        BUFFER_ADD("null");
-    } else if (ds->ds[i].type == DS_TYPE_COUNTER)
-      BUFFER_ADD("%" PRIu64, (uint64_t)vl->values[i].counter);
-    else if (ds->ds[i].type == DS_TYPE_DERIVE)
-      BUFFER_ADD("%" PRIi64, vl->values[i].derive);
-    else {
-      ERROR("format_json: Unknown data source type: %i", ds->ds[i].type);
-      sfree(rates);
-      return -1;
-    }
-  } /* for ds->ds_num */
-  BUFFER_ADD("]");
-
-#undef BUFFER_ADD
-
-  sfree(rates);
-  return 0;
-} /* }}} int values_to_json */
-
-static int dstypes_to_json(char *buffer, size_t buffer_size, /* {{{ */
-                           const data_set_t *ds) {
-  size_t offset = 0;
-
-  memset(buffer, 0, buffer_size);
-
-#define BUFFER_ADD(...)                                                        \
-  do {                                                                         \
-    int status;                                                                \
-    status = snprintf(buffer + offset, buffer_size - offset, __VA_ARGS__);     \
-    if (status < 1)                                                            \
-      return -1;                                                               \
-    else if (((size_t)status) >= (buffer_size - offset))                       \
-      return -ENOMEM;                                                          \
-    else                                                                       \
-      offset += ((size_t)status);                                              \
-  } while (0)
-
-  BUFFER_ADD("[");
-  for (size_t i = 0; i < ds->ds_num; i++) {
-    if (i > 0)
-      BUFFER_ADD(",");
-
-    BUFFER_ADD("\"%s\"", DS_TYPE_TO_STRING(ds->ds[i].type));
-  } /* for ds->ds_num */
-  BUFFER_ADD("]");
-
-#undef BUFFER_ADD
-
-  return 0;
-} /* }}} int dstypes_to_json */
-
-static int dsnames_to_json(char *buffer, size_t buffer_size, /* {{{ */
-                           const data_set_t *ds) {
-  size_t offset = 0;
-
-  memset(buffer, 0, buffer_size);
-
-#define BUFFER_ADD(...)                                                        \
-  do {                                                                         \
-    int status;                                                                \
-    status = snprintf(buffer + offset, buffer_size - offset, __VA_ARGS__);     \
-    if (status < 1)                                                            \
-      return -1;                                                               \
-    else if (((size_t)status) >= (buffer_size - offset))                       \
-      return -ENOMEM;                                                          \
-    else                                                                       \
-      offset += ((size_t)status);                                              \
-  } while (0)
-
-  BUFFER_ADD("[");
-  for (size_t i = 0; i < ds->ds_num; i++) {
-    if (i > 0)
-      BUFFER_ADD(",");
-
-    BUFFER_ADD("\"%s\"", ds->ds[i].name);
-  } /* for ds->ds_num */
-  BUFFER_ADD("]");
-
-#undef BUFFER_ADD
-
-  return 0;
-} /* }}} int dsnames_to_json */
-
-static int meta_data_keys_to_json(char *buffer, size_t buffer_size, /* {{{ */
-                                  meta_data_t *meta, char **keys,
-                                  size_t keys_num) {
-  size_t offset = 0;
-  int status;
-
-  buffer[0] = 0;
-
-#define BUFFER_ADD(...)                                                        \
-  do {                                                                         \
-    status = snprintf(buffer + offset, buffer_size - offset, __VA_ARGS__);     \
-    if (status < 1)                                                            \
-      return -1;                                                               \
-    else if (((size_t)status) >= (buffer_size - offset))                       \
-      return -ENOMEM;                                                          \
-    else                                                                       \
-      offset += ((size_t)status);                                              \
-  } while (0)
-
-  for (size_t i = 0; i < keys_num; ++i) {
-    int type;
-    char *key = keys[i];
-
-    type = meta_data_type(meta, key);
-    if (type == MD_TYPE_STRING) {
-      char *value = NULL;
-      if (meta_data_get_string(meta, key, &value) == 0) {
-        char temp[512] = "";
-
-        status = json_escape_string(temp, sizeof(temp), value);
-        sfree(value);
-        if (status != 0)
-          return status;
-
-        BUFFER_ADD(",\"%s\":%s", key, temp);
-      }
-    } else if (type == MD_TYPE_SIGNED_INT) {
-      int64_t value = 0;
-      if (meta_data_get_signed_int(meta, key, &value) == 0)
-        BUFFER_ADD(",\"%s\":%" PRIi64, key, value);
-    } else if (type == MD_TYPE_UNSIGNED_INT) {
-      uint64_t value = 0;
-      if (meta_data_get_unsigned_int(meta, key, &value) == 0)
-        BUFFER_ADD(",\"%s\":%" PRIu64, key, value);
-    } else if (type == MD_TYPE_DOUBLE) {
-      double value = 0.0;
-      if (meta_data_get_double(meta, key, &value) == 0)
-        BUFFER_ADD(",\"%s\":%f", key, value);
-    } else if (type == MD_TYPE_BOOLEAN) {
-      bool value = false;
-      if (meta_data_get_boolean(meta, key, &value) == 0)
-        BUFFER_ADD(",\"%s\":%s", key, value ? "true" : "false");
-    }
-  } /* for (keys) */
-
-  if (offset == 0)
-    return ENOENT;
-
-  buffer[0] = '{'; /* replace leading ',' */
-  BUFFER_ADD("}");
-
-#undef BUFFER_ADD
-
-  return 0;
-} /* }}} int meta_data_keys_to_json */
-
-static int meta_data_to_json(char *buffer, size_t buffer_size, /* {{{ */
-                             meta_data_t *meta) {
-  char **keys = NULL;
-  size_t keys_num;
-  int status;
-
-  if ((buffer == NULL) || (buffer_size == 0) || (meta == NULL))
-    return EINVAL;
-
-  status = meta_data_toc(meta, &keys);
-  if (status <= 0)
-    return status;
-  keys_num = (size_t)status;
 
-  status = meta_data_keys_to_json(buffer, buffer_size, meta, keys, keys_num);
-
-  for (size_t i = 0; i < keys_num; ++i)
-    sfree(keys[i]);
-  sfree(keys);
-
-  return status;
-} /* }}} int meta_data_to_json */
-
-static int value_list_to_json(char *buffer, size_t buffer_size, /* {{{ */
-                              const data_set_t *ds, const value_list_t *vl,
-                              int store_rates) {
-  char temp[512];
-  size_t offset = 0;
-  int status;
-
-  memset(buffer, 0, buffer_size);
-
-#define BUFFER_ADD(...)                                                        \
-  do {                                                                         \
-    status = snprintf(buffer + offset, buffer_size - offset, __VA_ARGS__);     \
-    if (status < 1)                                                            \
-      return -1;                                                               \
-    else if (((size_t)status) >= (buffer_size - offset))                       \
-      return -ENOMEM;                                                          \
-    else                                                                       \
-      offset += ((size_t)status);                                              \
-  } while (0)
-
-  /* All value lists have a leading comma. The first one will be replaced with
-   * a square bracket in `format_json_finalize'. */
-  BUFFER_ADD(",{");
-
-  status = values_to_json(temp, sizeof(temp), ds, vl, store_rates);
-  if (status != 0)
-    return status;
-  BUFFER_ADD("\"values\":%s", temp);
-
-  status = dstypes_to_json(temp, sizeof(temp), ds);
-  if (status != 0)
-    return status;
-  BUFFER_ADD(",\"dstypes\":%s", temp);
-
-  status = dsnames_to_json(temp, sizeof(temp), ds);
-  if (status != 0)
-    return status;
-  BUFFER_ADD(",\"dsnames\":%s", temp);
-
-  BUFFER_ADD(",\"time\":%.3f", CDTIME_T_TO_DOUBLE(vl->time));
-  BUFFER_ADD(",\"interval\":%.3f", CDTIME_T_TO_DOUBLE(vl->interval));
-
-#define BUFFER_ADD_KEYVAL(key, value)                                          \
-  do {                                                                         \
-    status = json_escape_string(temp, sizeof(temp), (value));                  \
-    if (status != 0)                                                           \
-      return status;                                                           \
-    BUFFER_ADD(",\"%s\":%s", (key), temp);                                     \
-  } while (0)
-
-  BUFFER_ADD_KEYVAL("host", vl->host);
-  BUFFER_ADD_KEYVAL("plugin", vl->plugin);
-  BUFFER_ADD_KEYVAL("plugin_instance", vl->plugin_instance);
-  BUFFER_ADD_KEYVAL("type", vl->type);
-  BUFFER_ADD_KEYVAL("type_instance", vl->type_instance);
-
-  if (vl->meta != NULL) {
-    char meta_buffer[buffer_size];
-    memset(meta_buffer, 0, sizeof(meta_buffer));
-    status = meta_data_to_json(meta_buffer, sizeof(meta_buffer), vl->meta);
-    if (status != 0)
-      return status;
-
-    BUFFER_ADD(",\"meta\":%s", meta_buffer);
-  } /* if (vl->meta != NULL) */
-
-  BUFFER_ADD("}");
-
-#undef BUFFER_ADD_KEYVAL
-#undef BUFFER_ADD
-
-  return 0;
-} /* }}} int value_list_to_json */
-
-static int format_json_value_list_nocheck(char *buffer, /* {{{ */
-                                          size_t *ret_buffer_fill,
-                                          size_t *ret_buffer_free,
-                                          const data_set_t *ds,
-                                          const value_list_t *vl,
-                                          int store_rates, size_t temp_size) {
-  char temp[temp_size];
-  int status;
-
-  status = value_list_to_json(temp, sizeof(temp), ds, vl, store_rates);
-  if (status != 0)
-    return status;
-  temp_size = strlen(temp);
-
-  memcpy(buffer + (*ret_buffer_fill), temp, temp_size + 1);
-  (*ret_buffer_fill) += temp_size;
-  (*ret_buffer_free) -= temp_size;
-
-  return 0;
-} /* }}} int format_json_value_list_nocheck */
-
-int format_json_initialize(char *buffer, /* {{{ */
-                           size_t *ret_buffer_fill, size_t *ret_buffer_free) {
-  size_t buffer_fill;
-  size_t buffer_free;
-
-  if ((buffer == NULL) || (ret_buffer_fill == NULL) ||
-      (ret_buffer_free == NULL))
-    return -EINVAL;
-
-  buffer_fill = *ret_buffer_fill;
-  buffer_free = *ret_buffer_free;
-
-  buffer_free = buffer_fill + buffer_free;
-  buffer_fill = 0;
-
-  if (buffer_free < 3)
-    return -ENOMEM;
-
-  memset(buffer, 0, buffer_free);
-  *ret_buffer_fill = buffer_fill;
-  *ret_buffer_free = buffer_free;
-
-  return 0;
-} /* }}} int format_json_initialize */
-
-int format_json_finalize(char *buffer, /* {{{ */
-                         size_t *ret_buffer_fill, size_t *ret_buffer_free) {
-  size_t pos;
-
-  if ((buffer == NULL) || (ret_buffer_fill == NULL) ||
-      (ret_buffer_free == NULL))
-    return -EINVAL;
-
-  if (*ret_buffer_free < 2)
-    return -ENOMEM;
-
-  /* Replace the leading comma added in `value_list_to_json' with a square
-   * bracket. */
-  if (buffer[0] != ',')
-    return -EINVAL;
-  buffer[0] = '[';
-
-  pos = *ret_buffer_fill;
-  buffer[pos] = ']';
-  buffer[pos + 1] = 0;
-
-  (*ret_buffer_fill)++;
-  (*ret_buffer_free)--;
-
-  return 0;
-} /* }}} int format_json_finalize */
-
-int format_json_value_list(char *buffer, /* {{{ */
-                           size_t *ret_buffer_fill, size_t *ret_buffer_free,
-                           const data_set_t *ds, const value_list_t *vl,
-                           int store_rates) {
-  if ((buffer == NULL) || (ret_buffer_fill == NULL) ||
-      (ret_buffer_free == NULL) || (ds == NULL) || (vl == NULL))
-    return -EINVAL;
-
-  if (*ret_buffer_free < 3)
-    return -ENOMEM;
-
-  return format_json_value_list_nocheck(buffer, ret_buffer_fill,
-                                        ret_buffer_free, ds, vl, store_rates,
-                                        (*ret_buffer_free) - 2);
-} /* }}} int format_json_value_list */
-
-#if HAVE_LIBYAJL
 static int json_add_string(yajl_gen g, char const *str) /* {{{ */
 {
   if (str == NULL)
@@ -582,6 +158,20 @@ static int format_metric(yajl_gen g, metric_t const *m) {
 }
 
 /* json_metric_family that all metrics in ml have the same name and value_type.
+ *
+ * Example:
+     [
+       {
+         "name": "roshi_select_call_count",
+         "help": "How many select calls have been made.",
+         "type": "COUNTER",
+         "metrics": [
+           {
+             "value": "1063110"
+           }
+         ]
+       }
+     ]
  */
 static int json_metric_family(yajl_gen g, metric_family_t const *fam) {
   CHECK_SUCCESS(yajl_gen_map_open(g)); /* BEGIN metric family */
@@ -663,21 +253,41 @@ int format_json_metric_family(strbuf_t *buf, metric_family_t const *fam,
   /* copy to output buffer */
   unsigned char const *out = NULL;
 #if HAVE_YAJL_V2
-  size_t unused_out_len = 0;
+  size_t out_len = 0;
 #else
-  unsigned int unused_out_len = 0;
+  unsigned int out_len = 0;
 #endif
-  if (yajl_gen_get_buf(g, &out, &unused_out_len) != yajl_gen_status_ok) {
+  if (yajl_gen_get_buf(g, &out, &out_len) != yajl_gen_status_ok) {
     yajl_gen_clear(g);
     yajl_gen_free(g);
     return -1;
   }
+
+  if (buf->fixed) {
+    size_t avail = (buf->size == 0) ? 0 : buf->size - (buf->pos + 1);
+    if (avail < out_len) {
+      yajl_gen_clear(g);
+      yajl_gen_free(g);
+      return ENOBUFS;
+    }
+  }
+
+  /* If the buffer is not empty, append by converting the closing ']' of "buf"
+   * to a comma and skip the opening '[' of "out". */
+  if (buf->pos != 0) {
+    assert(buf->ptr[buf->pos - 1] == ']');
+    buf->ptr[buf->pos - 1] = ',';
+
+    assert(out[0] == '[');
+    out++;
+  }
+
   status = strbuf_print(buf, (void *)out);
 
   yajl_gen_clear(g);
   yajl_gen_free(g);
   return status;
-} /* }}} format_json_metric */
+} /* }}} format_json_metric_family */
 
 static int format_alert(yajl_gen g, notification_t const *n) /* {{{ */
 {
@@ -822,10 +432,3 @@ int format_json_notification(char *buffer, size_t buffer_size, /* {{{ */
   yajl_gen_free(g);
   return 0;
 } /* }}} format_json_notification */
-#else
-int format_json_notification(char *buffer, size_t buffer_size, /* {{{ */
-                             notification_t const *n) {
-  ERROR("format_json_notification: Not available (requires libyajl).");
-  return ENOTSUP;
-} /* }}} int format_json_notification */
-#endif
index eb05d10653cf42ceb7815cad3e1b25a7f599f38d..12cc969f0e8bcf5b8e20fc049882c7d588c63ac6 100644 (file)
@@ -1,6 +1,6 @@
 /**
  * collectd - src/utils_format_json.h
- * Copyright (C) 2009       Florian octo Forster
+ * Copyright (C) 2009-2020  Florian octo Forster
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
 #define JSON_GAUGE_FORMAT GAUGE_FORMAT
 #endif
 
-int format_json_initialize(char *buffer, size_t *ret_buffer_fill,
-                           size_t *ret_buffer_free);
-int format_json_value_list(char *buffer, size_t *ret_buffer_fill,
-                           size_t *ret_buffer_free, const data_set_t *ds,
-                           const value_list_t *vl, int store_rates);
-int format_json_finalize(char *buffer, size_t *ret_buffer_fill,
-                         size_t *ret_buffer_free);
-
-/* format_json_metric writes m to buf in JSON format. The format produces is
- * compatible to the "prometheus/prom2json" project. */
+/* format_json_metric_family adds the metric family "fam" to the buffer "buf"
+ * in JSON format. The format produced is compatible to the
+ * "prometheus/prom2json" project. Calling this function repeatedly with the
+ * same buffer will append additional metric families to the buffer. If the
+ * buffer has fixed size and the serialized metric family exceeds the buffer
+ * length, the buffer is unmodified and ENOBUFS is returned. */
 int format_json_metric_family(strbuf_t *buf, metric_family_t const *fam,
                               bool store_rates);
 
index 3185b725e9419cc5f7e741d00d4eeb3dd354a524..6251ecd25222a15cd0cee923a73588ae53c9eb3c 100644 (file)
 #endif
 
 typedef struct {
-  char const *key;
-  char const *value;
-} keyval_t;
-
-typedef struct {
-  keyval_t *expected_labels;
+  label_t *expected_labels;
   size_t expected_labels_num;
 
   keyval_t *current_label;
@@ -63,9 +58,9 @@ static int test_map_key(void *ctx, unsigned char const *key,
 
   c->current_label = NULL;
   for (i = 0; i < c->expected_labels_num; i++) {
-    keyval_t *l = c->expected_labels + i;
-    if ((strlen(l->key) == key_len) &&
-        (strncmp(l->key, (char const *)key, key_len) == 0)) {
+    label_t *l = c->expected_labels + i;
+    if ((strlen(l->name) == key_len) &&
+        (strncmp(l->name, (char const *)key, key_len) == 0)) {
       c->current_label = l;
       break;
     }
@@ -106,7 +101,7 @@ static int test_string(void *ctx, unsigned char const *value,
     memmove(got, value, value_len);
     got[value_len] = 0;
 
-    status = expect_label(l->key, got, l->value);
+    status = expect_label(l->name, got, l->value);
 
     free(got);
 
@@ -165,49 +160,49 @@ DEF_TEST(notification) {
   return expect_json_labels(got, labels, STATIC_ARRAY_SIZE(labels));
 }
 
-DEF_TEST(metric) {
+DEF_TEST(metric_family) {
   struct {
     char const *identity;
+    metric_type_t type;
     value_t value;
-    int value_type;
     cdtime_t time;
     cdtime_t interval;
-    meta_data_t *meta;
     char const *want;
   } cases[] = {
-      {
-          .identity = "metric_name",
-          .value = (value_t){.gauge = 42},
-          .value_type = VALUE_TYPE_GAUGE,
-          .want = "[{\"name\":\"metric_name\",\"type\":\"GAUGE\",\"metrics\":["
-                  "{\"value\":\"42\"}"
-                  "]}]",
-      },
-      {
-          .identity =
-              "metric_with_labels{sorted=\"true\",alphabetically=\"yes\"}",
-          .value = (value_t){.gauge = 42},
-          .value_type = VALUE_TYPE_GAUGE,
-          .want = "[{\"name\":\"metric_with_labels\",\"type\":\"GAUGE\","
-                  "\"metrics\":["
-                  "{\"labels\":{\"alphabetically\":\"yes\",\"sorted\":\"true\"}"
-                  ",\"value\":\"42\"}"
-                  "]}]",
-      },
-      {
-          .identity = "metric_with_time",
-          .value = (value_t){.gauge = 42},
-          .value_type = VALUE_TYPE_GAUGE,
-          .time = MS_TO_CDTIME_T(1592987324125),
-          .want =
-              "[{\"name\":\"metric_with_time\",\"type\":\"GAUGE\",\"metrics\":["
-              "{\"timestamp_ms\":\"1592987324125\",\"value\":\"42\"}"
-              "]}]",
-      },
+    {
+        .identity = "metric_name",
+        .value.gauge = 42,
+        .type = METRIC_TYPE_GAUGE,
+        .want = "[{\"name\":\"metric_name\",\"type\":\"GAUGE\",\"metrics\":["
+                "{\"value\":\"42\"}"
+                "]}]",
+    },
+    {
+        .identity =
+            "metric_with_labels{sorted=\"true\",alphabetically=\"yes\"}",
+        .value.gauge = 42,
+        .type = METRIC_TYPE_GAUGE,
+        .want = "[{\"name\":\"metric_with_labels\",\"type\":\"GAUGE\","
+                "\"metrics\":["
+                "{\"labels\":{\"alphabetically\":\"yes\",\"sorted\":\"true\"}"
+                ",\"value\":\"42\"}"
+                "]}]",
+    },
+    {
+        .identity = "metric_with_time",
+        .value.gauge = 42,
+        .type = METRIC_TYPE_GAUGE,
+        .time = MS_TO_CDTIME_T(1592987324125),
+        .want =
+            "[{\"name\":\"metric_with_time\",\"type\":\"GAUGE\",\"metrics\":["
+            "{\"timestamp_ms\":\"1592987324125\",\"value\":\"42\"}"
+            "]}]",
+    },
+#if 0
       {
           .identity = "derive_max",
-          .value = (value_t){.derive = INT64_MAX},
-          .value_type = VALUE_TYPE_DERIVE,
+          .value.derive = INT64_MAX,
+          .type = METRIC_TYPE_COUNTER,
           .want = "[{\"name\":\"derive_max\",\"type\":\"COUNTER\",\"metrics\":["
                   "{\"value\":\"9223372036854775807\"}"
                   "]}]",
@@ -215,37 +210,35 @@ DEF_TEST(metric) {
       {
           .identity = "derive_min",
           .value = (value_t){.derive = INT64_MIN},
-          .value_type = VALUE_TYPE_DERIVE,
+          .type = METRIC_TYPE_COUNTER,
           .want = "[{\"name\":\"derive_min\",\"type\":\"COUNTER\",\"metrics\":["
                   "{\"value\":\"-9223372036854775808\"}"
                   "]}]",
       },
-      {
-          .identity = "counter_max",
-          .value = (value_t){.counter = UINT64_MAX},
-          .value_type = DS_TYPE_COUNTER,
-          .want =
-              "[{\"name\":\"counter_max\",\"type\":\"COUNTER\",\"metrics\":["
-              "{\"value\":\"18446744073709551615\"}"
-              "]}]",
-      },
+#endif
+    {
+        .identity = "counter_max",
+        .value = (value_t){.counter = UINT64_MAX},
+        .type = METRIC_TYPE_COUNTER,
+        .want = "[{\"name\":\"counter_max\",\"type\":\"COUNTER\",\"metrics\":["
+                "{\"value\":\"18446744073709551615\"}"
+                "]}]",
+    },
   };
 
   for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
-    identity_t *id;
-    CHECK_NOT_NULL(id = identity_unmarshal_text(cases[i].identity));
-
-    metric_single_t m = {
-        .identity = id,
-        .value = cases[i].value,
-        .value_type = cases[i].value_type,
-        .time = cases[i].time,
-        .interval = cases[i].interval,
-        .meta = cases[i].meta,
-    };
+    metric_family_t *fam;
+    CHECK_NOT_NULL(
+        fam = metric_family_unmarshal_text(cases[i].identity, cases[i].type));
+
+    metric_t *m = fam->metric.ptr;
+
+    m->value = cases[i].value;
+    m->time = cases[i].time;
+    m->interval = cases[i].interval;
 
     strbuf_t buf = STRBUF_CREATE;
-    CHECK_ZERO(format_json_metric(&buf, &m, false));
+    CHECK_ZERO(format_json_metric_family(&buf, fam, false));
 
     EXPECT_EQ_STR(cases[i].want, buf.ptr);
     STRBUF_DESTROY(buf);
@@ -254,9 +247,50 @@ DEF_TEST(metric) {
   return 0;
 }
 
+DEF_TEST(metric_family_append) {
+  strbuf_t buf = STRBUF_CREATE;
+
+  metric_family_t fam = {
+      .name = "first",
+      .type = METRIC_TYPE_UNTYPED,
+  };
+  metric_family_metric_append(&fam, (metric_t){
+                                        .value.gauge = 0,
+                                    });
+  metric_family_metric_append(&fam, (metric_t){
+                                        .value.gauge = 1,
+                                    });
+  CHECK_ZERO(format_json_metric_family(&buf, &fam, false));
+  EXPECT_EQ_STR("[{\"name\":\"first\",\"type\":\"UNTYPED\",\"metrics\":[{"
+                "\"value\":\"0\"},{\"value\":\"1\"}]}]",
+                buf.ptr);
+
+  metric_family_metric_reset(&fam);
+
+  fam = (metric_family_t){
+      .name = "second",
+      .type = METRIC_TYPE_GAUGE,
+  };
+  metric_family_metric_append(&fam, (metric_t){
+                                        .value.gauge = 2,
+                                    });
+
+  CHECK_ZERO(format_json_metric_family(&buf, &fam, false));
+  EXPECT_EQ_STR("[{\"name\":\"first\",\"type\":\"UNTYPED\",\"metrics\":[{"
+                "\"value\":\"0\"},{\"value\":\"1\"}]},{\"name\":\"second\","
+                "\"type\":\"GAUGE\",\"metrics\":[{\"value\":\"2\"}]}]",
+                buf.ptr);
+
+  metric_family_metric_reset(&fam);
+  STRBUF_DESTROY(buf);
+
+  return 0;
+}
+
 int main(void) {
   RUN_TEST(notification);
-  RUN_TEST(metric);
+  RUN_TEST(metric_family);
+  RUN_TEST(metric_family_append);
 
   END_TEST;
 }