src/utils_parse_json.c: Implement parse_json().
authorFlorian Forster <octo@collectd.org>
Fri, 12 Jun 2015 18:40:16 +0000 (19:40 +0100)
committerFlorian Forster <octo@collectd.org>
Mon, 8 Oct 2018 08:24:58 +0000 (10:24 +0200)
This allows us to parse JSON encoded value lists, as emitted
src/utils_format_json.c.

Makefile.am
src/utils_parse_json.c [new file with mode: 0644]
src/utils_parse_json.h [new file with mode: 0644]
src/utils_parse_json_test.c [new file with mode: 0644]

index aed8eef..39f1d3a 100644 (file)
@@ -445,6 +445,25 @@ test_format_json_LDADD = \
        -lm
 endif
 
+if BUILD_WITH_LIBYAJL2
+EXTRA_LTLIBRARIES += libparse_json.la
+libparse_json_la_SOURCES = \
+       src/utils_parse_json.c \
+       src/utils_parse_json.h
+libparse_json_la_CPPFLAGS = $(AM_CPPFLAGS) $(BUILD_WITH_LIBYAJL_CPPFLAGS)
+libparse_json_la_LDFLAGS  = $(AM_LDFLAGS) $(BUILD_WITH_LIBYAJL_LDFLAGS)
+libparse_json_la_LIBADD   = $(BUILD_WITH_LIBYAJL_LIBS)
+
+check_PROGRAMS += test_parse_json
+
+test_parse_json_SOURCES = \
+       src/utils_parse_json_test.c \
+       src/testing.h
+test_parse_json_LDADD = \
+       libparse_json.la \
+       libplugin_mock.la
+endif
+
 libstrbuf_la_SOURCES = \
        src/utils_strbuf.c \
        src/utils_strbuf.h
diff --git a/src/utils_parse_json.c b/src/utils_parse_json.c
new file mode 100644 (file)
index 0000000..db2cee4
--- /dev/null
@@ -0,0 +1,285 @@
+/**
+ * collectd - src/utils_parse_json.c
+ * Copyright (C) 2018  Florian Forster
+ *
+ * MIT License
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * Authors:
+ *   Florian Forster <octo at collectd.org>
+ **/
+
+#include "collectd.h"
+
+#include "common.h"
+#include "plugin.h"
+#include "utils_parse_json.h"
+
+#include <yajl/yajl_tree.h>
+
+static int parse_json_gauge(yajl_val value_val, value_t *v) /* {{{ */
+{
+  if (YAJL_IS_DOUBLE(value_val))
+    v->gauge = (gauge_t)YAJL_GET_DOUBLE(value_val);
+  else if (YAJL_IS_NULL(value_val))
+    v->gauge = NAN;
+  else
+    return EINVAL;
+
+  return 0;
+} /* }}} int parse_json_gauge */
+
+static int parse_json_derive(yajl_val value_val, value_t *v) /* {{{ */
+{
+  if (!YAJL_IS_INTEGER(value_val)) {
+    if (YAJL_IS_NUMBER(value_val)) {
+      WARNING("parse_json_derive: %s is not an integer. The most common cause "
+              "is that the publisher has the StoreRates option enabled.",
+              YAJL_GET_NUMBER(value_val));
+    }
+    return EINVAL;
+  }
+
+  v->derive = (derive_t)YAJL_GET_INTEGER(value_val);
+  return 0;
+} /* }}} int parse_json_derive */
+
+static int parse_json_counter(yajl_val value_val, value_t *v) /* {{{ */
+{
+  if (!YAJL_IS_INTEGER(value_val)) {
+    if (YAJL_IS_NUMBER(value_val)) {
+      WARNING("parse_json_counter: %s is not an integer. The most common cause "
+              "is that the publisher has the StoreRates option enabled.",
+              YAJL_GET_NUMBER(value_val));
+    }
+    return EINVAL;
+  }
+
+  v->counter = (counter_t)YAJL_GET_INTEGER(value_val);
+  return 0;
+} /* }}} int parse_json_counter */
+
+static int parse_json_absolute(yajl_val value_val, value_t *v) /* {{{ */
+{
+  if (!YAJL_IS_INTEGER(value_val)) {
+    if (YAJL_IS_NUMBER(value_val)) {
+      WARNING("parse_json_absolute: %s is not an integer. The most common "
+              "cause is that the publisher has the StoreRates option enabled.",
+              YAJL_GET_NUMBER(value_val));
+    }
+    return EINVAL;
+  }
+
+  v->absolute = (absolute_t)YAJL_GET_INTEGER(value_val);
+  return 0;
+} /* }}} int parse_json_absolute */
+
+static int parse_json_value(yajl_val value_val, yajl_val dstype_val,
+                            value_t *v) /* {{{ */
+{
+  char const *dstype = YAJL_GET_STRING(dstype_val);
+
+  if (dstype == NULL) {
+    NOTICE("parse_json_value: unable to get data source type");
+    return EINVAL;
+  } else if (strcasecmp("gauge", dstype) == 0)
+    return parse_json_gauge(value_val, v);
+  else if (strcasecmp("derive", dstype) == 0)
+    return parse_json_derive(value_val, v);
+  else if (strcasecmp("counter", dstype) == 0)
+    return parse_json_counter(value_val, v);
+  else if (strcasecmp("absolute", dstype) == 0)
+    return parse_json_absolute(value_val, v);
+  else {
+    NOTICE("parse_json_value: unexpected data source type \"%s\"", dstype);
+    return EINVAL;
+  }
+} /* }}} int parse_json_value */
+
+static int parse_json_values(yajl_val root, value_list_t *vl) /* {{{ */
+{
+  char const *values_path[] = {"values", NULL};
+  char const *dstypes_path[] = {"dstypes", NULL};
+
+  yajl_val values_val = yajl_tree_get(root, values_path, yajl_t_array);
+  yajl_val dstypes_val = yajl_tree_get(root, dstypes_path, yajl_t_array);
+
+  size_t i;
+
+  if (values_val == NULL) {
+    NOTICE("parse_json_values: key \"values\" not found (or not an array)");
+    return EINVAL;
+  }
+  if (dstypes_val == NULL) {
+    NOTICE("parse_json_values: key \"dstypes\" not found (or not an array)");
+    return EINVAL;
+  }
+  if (YAJL_GET_ARRAY(values_val)->len != YAJL_GET_ARRAY(dstypes_val)->len) {
+    NOTICE("parse_json_values: lengths of \"values\" (%zu) and "
+           "\"dstypes\" (%zu) differ",
+           YAJL_GET_ARRAY(values_val)->len, YAJL_GET_ARRAY(dstypes_val)->len);
+    return EINVAL;
+  }
+
+  vl->values = calloc(YAJL_GET_ARRAY(values_val)->len, sizeof(*vl->values));
+  if (vl->values == NULL)
+    return ENOMEM;
+  vl->values_len = (int)YAJL_GET_ARRAY(values_val)->len;
+
+  for (i = 0; i < YAJL_GET_ARRAY(values_val)->len; i++) {
+    int status = parse_json_value(YAJL_GET_ARRAY(values_val)->values[i],
+                                  YAJL_GET_ARRAY(dstypes_val)->values[i],
+                                  &vl->values[i]);
+    if (status != 0) {
+      sfree(vl->values);
+      vl->values = NULL;
+      vl->values_len = 0;
+      return status;
+    }
+  }
+
+  return 0;
+} /* }}} int parse_json_values */
+
+static int parse_json_string(yajl_val root, char *key, char *buffer,
+                             size_t buffer_size) /* {{{ */
+{
+  char const *path[] = {key, NULL};
+  yajl_val string_val = yajl_tree_get(root, path, yajl_t_string);
+
+  if (!YAJL_IS_STRING(string_val))
+    return EINVAL;
+
+  sstrncpy(buffer, YAJL_GET_STRING(string_val), buffer_size);
+  return 0;
+} /* }}} int parse_json_string */
+
+static int parse_json_time(yajl_val root, char *key, cdtime_t *t) /* {{{ */
+{
+  char const *path[] = {key, NULL};
+  yajl_val double_val = yajl_tree_get(root, path, yajl_t_number);
+
+  if (!YAJL_IS_DOUBLE(double_val))
+    return EINVAL;
+
+  *t = DOUBLE_TO_CDTIME_T(YAJL_GET_DOUBLE(double_val));
+  return 0;
+} /* }}} int parse_json_time */
+
+static int parse_json_vl(yajl_val root, value_list_t *vl) /* {{{ */
+{
+  int status;
+
+  memset(vl, 0, sizeof(*vl));
+
+  status = parse_json_string(root, "host", vl->host, sizeof(vl->host));
+  if (status != 0) {
+    NOTICE("parse_json: parsing key \"host\" failed");
+    return status;
+  }
+
+  status = parse_json_string(root, "plugin", vl->plugin, sizeof(vl->plugin));
+  if (status != 0) {
+    NOTICE("parse_json: parsing key \"plugin\" failed");
+    return status;
+  }
+  parse_json_string(root, "plugin_instance", vl->plugin_instance,
+                    sizeof(vl->plugin_instance));
+
+  status = parse_json_string(root, "type", vl->type, sizeof(vl->type));
+  if (status != 0) {
+    NOTICE("parse_json: parsing key \"type\" failed");
+    return status;
+  }
+  parse_json_string(root, "type_instance", vl->type_instance,
+                    sizeof(vl->type_instance));
+
+  status = parse_json_time(root, "time", &vl->time);
+  if (status != 0) {
+    NOTICE("parse_json: parsing key \"time\" failed");
+    return status;
+  }
+
+  status = parse_json_time(root, "interval", &vl->interval);
+  if (status != 0) {
+    NOTICE("parse_json: parsing key \"interval\" failed");
+    return status;
+  }
+
+  status = parse_json_values(root, vl);
+  if (status != 0) {
+    return status;
+  }
+
+  return 0;
+} /* }}} int parse_json_vl */
+
+int parse_json(char const *json, value_list_t ***ret_vls,
+               size_t *ret_vls_num) /* {{{ */
+{
+  yajl_val root;
+  char errbuf[1024];
+  size_t i;
+
+  value_list_t **vls = NULL;
+  size_t vls_num = 0;
+
+  root = yajl_tree_parse(json, errbuf, sizeof(errbuf));
+  if (root == NULL) {
+    ERROR("parse_json: yajl_tree_parse failed: %s", errbuf);
+    return -1;
+  }
+
+  if (!YAJL_IS_ARRAY(root)) {
+    NOTICE("parse_json: root is not an array");
+    return -1;
+  }
+
+  for (i = 0; i < YAJL_GET_ARRAY(root)->len; i++) {
+    yajl_val vl_json = YAJL_GET_ARRAY(root)->values[i];
+    value_list_t *vl;
+
+    vl = malloc(sizeof(*vl));
+    if (vl == NULL)
+      continue;
+
+    int status = parse_json_vl(vl_json, vl);
+    if (status != 0) {
+      sfree(vl);
+      continue;
+    }
+
+    value_list_t **tmp = realloc(vls, sizeof(*vls) * (vls_num + 1));
+    if (tmp == NULL) {
+      ERROR("parse_json: realloc failed");
+      sfree(vl->values);
+      sfree(vl);
+      continue;
+    }
+    vls = tmp;
+
+    vls[vls_num] = vl;
+    vls_num++;
+  }
+
+  *ret_vls = vls;
+  *ret_vls_num = vls_num;
+  return 0;
+} /* }}} int parse_json */
diff --git a/src/utils_parse_json.h b/src/utils_parse_json.h
new file mode 100644 (file)
index 0000000..cb56d64
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * collectd - src/utils_parse_json.h
+ * Copyright (C) 2018  Florian Forster
+ *
+ * MIT License
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * Authors:
+ *   Florian Forster <octo at collectd.org>
+ **/
+
+#ifndef UTILS_PARSE_JSON_H
+#define UTILS_PARSE_JSON_H 1
+
+#include "collectd.h"
+
+#include "plugin.h"
+
+int parse_json(char const *json, value_list_t ***vls, size_t *vls_num);
+
+#endif
diff --git a/src/utils_parse_json_test.c b/src/utils_parse_json_test.c
new file mode 100644 (file)
index 0000000..761e0c0
--- /dev/null
@@ -0,0 +1,190 @@
+/**
+ * collectd - src/tests/utils_parse_json_test.c
+ * Copyright (C) 2018  Florian Forster
+ *
+ * MIT License
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * Authors:
+ *   Florian Forster <octo at collectd.org>
+ **/
+
+#include "utils_parse_json.h"
+#include "testing.h"
+
+DEF_TEST(single_value) /* {{{ */
+{
+  char const *json =
+      "["
+      /* gauge */
+      "{\"host\":\"example.com\",\"plugin\":\"test\",\"type\":\"answer\","
+      "\"time\":1434357493.398,\"interval\":10.000,"
+      "\"dstypes\":[\"gauge\"],\"values\":[42.0]},"
+      /* gauge (NaN) */
+      "{\"host\":\"example.com\",\"plugin\":\"test\",\"type\":\"gauge\","
+      "\"time\":1434357493.398,\"interval\":10.000,"
+      "\"dstypes\":[\"gauge\"],\"values\":[null]},"
+      /* derive */
+      "{\"host\":\"example.com\",\"plugin\":\"test\",\"type\":\"total_things\","
+      "\"plugin_instance\":\"foo\",\"type_instance\":\"bar\","
+      "\"time\":1434357493.398,\"interval\":10.000,"
+      "\"dstypes\":[\"derive\"],\"values\":[31337]}"
+      "]";
+
+  value_list_t **vls = NULL;
+  size_t vls_num = 0;
+
+  CHECK_ZERO(parse_json(json, &vls, &vls_num));
+  OK(vls_num == 3);
+  if (vls_num != 3)
+    return (-1);
+
+  value_list_t *vl = vls[0];
+  EXPECT_EQ_STR("example.com", vl->host);
+  EXPECT_EQ_STR("test", vl->plugin);
+  EXPECT_EQ_STR("", vl->plugin_instance);
+  EXPECT_EQ_STR("answer", vl->type);
+  EXPECT_EQ_STR("", vl->type_instance);
+  OK(vl->time == 1540129631229236480);
+  OK(vl->interval == 10737418240);
+  OK(vl->values_len == 1);
+  OK(vl->values[0].gauge == 42.0);
+
+  vl = vls[1];
+  OK(vl->values_len == 1);
+  OK(isnan(vl->values[0].gauge));
+
+  vl = vls[2];
+  EXPECT_EQ_STR("example.com", vl->host);
+  EXPECT_EQ_STR("test", vl->plugin);
+  EXPECT_EQ_STR("foo", vl->plugin_instance);
+  EXPECT_EQ_STR("total_things", vl->type);
+  EXPECT_EQ_STR("bar", vl->type_instance);
+  OK(vl->time == 1540129631229236480);
+  OK(vl->interval == 10737418240);
+  OK(vl->values_len == 1);
+  OK(vl->values[0].derive == 31337);
+
+  free(vls[0]->values);
+  free(vls[0]);
+  free(vls[1]->values);
+  free(vls[1]);
+  free(vls);
+  return (0);
+} /* }}} single_value */
+
+DEF_TEST(multi_value) /* {{{ */
+{
+  char const *json =
+      "["
+      "{\"host\":\"example.com\",\"plugin\":\"load\",\"type\":\"load\","
+      "\"time\":1434357493,\"interval\":10,"
+      "\"dstypes\":[\"gauge\",\"gauge\",\"gauge\"],\"values\":[1.12,0.56,0.64]}"
+      "]";
+
+  value_list_t **vls = NULL;
+  size_t vls_num = 0;
+
+  CHECK_ZERO(parse_json(json, &vls, &vls_num));
+  OK(vls_num == 1);
+
+  value_list_t *vl = vls[0];
+  EXPECT_EQ_STR("example.com", vl->host);
+  EXPECT_EQ_STR("load", vl->plugin);
+  EXPECT_EQ_STR("load", vl->type);
+  OK(vl->time == 1540129630801887232);
+  OK(vl->interval == 10737418240);
+
+  OK(vl->values_len == 3);
+  OK(vl->values[0].gauge == 1.12);
+  OK(vl->values[1].gauge == 0.56);
+  OK(vl->values[2].gauge == 0.64);
+
+  free(vl->values);
+  free(vl);
+  free(vls);
+  return (0);
+} /* }}} multi_value */
+
+DEF_TEST(failures) /* {{{ */
+{
+  char const *json =
+      "["
+      /* host missing */
+      "{\"plugin\":\"test\",\"type\":\"answer\","
+      "\"time\":1434357493.398,\"interval\":10.000,"
+      "\"dstypes\":[\"gauge\"],\"values\":[42.0]},"
+      /* plugin missing */
+      "{\"host\":\"example.com\",\"type\":\"answer\","
+      "\"time\":1434357493.398,\"interval\":10.000,"
+      "\"dstypes\":[\"gauge\"],\"values\":[42.0]},"
+      /* type missing */
+      "{\"host\":\"example.com\",\"plugin\":\"test\","
+      "\"time\":1434357493.398,\"interval\":10.000,"
+      "\"dstypes\":[\"gauge\"],\"values\":[42.0]},"
+      /* time missing */
+      "{\"host\":\"example.com\",\"plugin\":\"test\",\"type\":\"answer\","
+      "\"interval\":10.000,"
+      "\"dstypes\":[\"gauge\"],\"values\":[42.0]},"
+      /* interval missing */
+      "{\"host\":\"example.com\",\"plugin\":\"test\",\"type\":\"answer\","
+      "\"time\":1434357493.398,"
+      "\"dstypes\":[\"gauge\"],\"values\":[42.0]},"
+      /* derive -> floating point mismatch */
+      "{\"host\":\"example.com\",\"plugin\":\"test\",\"type\":\"answer\","
+      "\"time\":1434357493.398,\"interval\":10.000,"
+      "\"dstypes\":[\"derive\"],\"values\":[42.0]},"
+      /* len(dstypes) != len(values) */
+      "{\"host\":\"example.com\",\"plugin\":\"test\",\"type\":\"answer\","
+      "\"time\":1434357493.398,\"interval\":10.000,"
+      "\"dstypes\":[\"gauge\"],\"values\":[42.0, 23.0]},"
+      /* type mismatch: got boolean, want string */
+      "{\"host\":true,\"plugin\":\"test\",\"type\":\"answer\","
+      "\"time\":1434357493.398,\"interval\":10.000,"
+      "\"dstypes\":[\"gauge\"],\"values\":[42.0]},"
+      /* type mismatch: got boolean, want number */
+      "{\"host\":\"example.com\",\"plugin\":\"test\",\"type\":\"answer\","
+      "\"time\":true,\"interval\":10.000,"
+      "\"dstypes\":[\"gauge\"],\"values\":[42.0]},"
+      /* type mismatch: got string/number, want array/array */
+      "{\"host\":\"example.com\",\"plugin\":\"test\",\"type\":\"answer\","
+      "\"time\":1434357493.398,\"interval\":10.000,"
+      "\"dstypes\":\"gauge\",\"values\":42.0}"
+      "]";
+
+  value_list_t **vls = NULL;
+  size_t vls_num = 0;
+
+  CHECK_ZERO(parse_json(json, &vls, &vls_num));
+  CHECK_ZERO(vls_num);
+
+  return (0);
+} /* }}} failures */
+
+int main(int argc, char **argv) /* {{{ */
+{
+  RUN_TEST(single_value);
+  RUN_TEST(multi_value);
+  RUN_TEST(failures);
+
+  END_TEST;
+} /* }}} int main */
+
+/* vim: set sw=2 sts=2 et fdm=marker : */