src/utils_format_gcm.[ch]: Implementation of the Stackdriver Monitoring API v3.
authorFlorian Forster <octo@collectd.org>
Sat, 20 Jun 2015 13:54:54 +0000 (15:54 +0200)
committerFlorian Forster <octo@collectd.org>
Thu, 4 Oct 2018 19:14:36 +0000 (21:14 +0200)
Docs: https://cloud.google.com/monitoring/api/ref_v3/rest/

Makefile.am
src/daemon/utils_cache_mock.c
src/utils_format_gcm.c [new file with mode: 0644]
src/utils_format_gcm.h [new file with mode: 0644]
src/utils_format_gcm_test.c [new file with mode: 0644]

index a7e2f4d..aa8c59f 100644 (file)
@@ -611,6 +611,29 @@ libgce_la_LIBADD = \
        $(BUILD_WITH_LIBCURL_LIBS)
 endif
 
+if BUILD_WITH_LIBYAJL
+noinst_LTLIBRARIES += libformat_gcm.la
+libformat_gcm_la_SOURCES = \
+       src/utils_format_gcm.c \
+       src/utils_format_gcm.h
+libformat_gcm_la_CPPFLAGS = \
+       $(AM_CPPFLAGS) \
+       $(BUILD_WITH_LIBYAJL_CPPFLAGS)
+libformat_gcm_la_LIBADD = \
+       libavltree.la \
+       $(BUILD_WITH_LIBSSL_LIBS) \
+       $(BUILD_WITH_LIBYAJL_LIBS)
+
+check_PROGRAMS += test_format_gcm
+TESTS += test_format_gcm
+test_format_gcm_SOURCES = \
+       utils_format_gcm_test.c \
+       testing.h
+test_format_gcm_LDADD = \
+       libformat_gcm.la \
+       daemon/libplugin_mock.la \
+       -lm
+endif
 
 if BUILD_PLUGIN_AGGREGATION
 pkglib_LTLIBRARIES += aggregation.la
index 1495a80..0b8c997 100644 (file)
@@ -27,6 +27,8 @@
 #include "utils_cache.h"
 #include <errno.h>
 
+#include <errno.h>
+
 gauge_t *uc_get_rate(__attribute__((unused)) data_set_t const *ds,
                      __attribute__((unused)) value_list_t const *vl) {
   errno = ENOTSUP;
@@ -46,3 +48,13 @@ int uc_get_value_by_name(const char *name, value_t **ret_values,
                          size_t *ret_values_num) {
   return ENOTSUP;
 }
+
+int uc_meta_data_get_unsigned_int(const value_list_t *vl, const char *key,
+                                  uint64_t *value) {
+  return -ENOENT;
+}
+
+int uc_meta_data_add_unsigned_int(const value_list_t *vl, const char *key,
+                                  uint64_t value) {
+  return 0;
+}
diff --git a/src/utils_format_gcm.c b/src/utils_format_gcm.c
new file mode 100644 (file)
index 0000000..0c60ef0
--- /dev/null
@@ -0,0 +1,710 @@
+/**
+ * collectd - src/utils_format_gcm.c
+ * ISC license
+ *
+ * Copyright (C) 2017  Florian Forster
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ * Authors:
+ *   Florian Forster <octo at collectd.org>
+ **/
+
+#include "collectd.h"
+
+#include "utils_format_gcm.h"
+
+#include "common.h"
+#include "plugin.h"
+#include "utils_avltree.h"
+#include "utils_cache.h"
+#include "utils_time.h"
+
+#include <yajl/yajl_gen.h>
+#include <yajl/yajl_parse.h>
+#if HAVE_YAJL_YAJL_VERSION_H
+#include <yajl/yajl_version.h>
+#endif
+
+struct gcm_output_s {
+  gcm_resource_t *res;
+  yajl_gen gen;
+  c_avl_tree_t *staged;
+  c_avl_tree_t *metric_descriptors;
+};
+
+struct gcm_label_s {
+  char *key;
+  char *value;
+};
+typedef struct gcm_label_s gcm_label_t;
+
+struct gcm_resource_s {
+  char *type;
+
+  gcm_label_t *labels;
+  size_t labels_num;
+};
+
+static int json_string(yajl_gen gen, char const *s) /* {{{ */
+{
+  yajl_gen_status status =
+      yajl_gen_string(gen, (unsigned char const *)s, strlen(s));
+  if (status != yajl_gen_status_ok)
+    return (int)status;
+
+  return 0;
+} /* }}} int json_string */
+
+static int json_time(yajl_gen gen, cdtime_t t) {
+  char buffer[64];
+  size_t status;
+
+  status = rfc3339(buffer, sizeof(buffer), t);
+  if (status != 0) {
+    return status;
+  }
+
+  return json_string(gen, buffer);
+} /* }}} int json_time */
+
+/* MonitoredResource
+ *
+ * {
+ *   "type": "library.googleapis.com/book",
+ *   "labels": {
+ *     "/genre": "fiction",
+ *     "/media": "paper"
+ *     "/title": "The Old Man and the Sea"
+ *   }
+ * }
+ */
+static int format_gcm_resource(yajl_gen gen, gcm_resource_t *res) /* {{{ */
+{
+  int status;
+
+  yajl_gen_map_open(gen);
+
+  status = json_string(gen, "type") || json_string(gen, res->type);
+  if (status != 0)
+    return status;
+
+  if (res->labels_num != 0) {
+    size_t i;
+
+    status = json_string(gen, "labels");
+    if (status != 0)
+      return status;
+
+    yajl_gen_map_open(gen);
+    for (i = 0; i < res->labels_num; i++) {
+      status = json_string(gen, res->labels[i].key) ||
+               json_string(gen, res->labels[i].value);
+      if (status != 0)
+        return status;
+    }
+    yajl_gen_map_close(gen);
+  }
+
+  yajl_gen_map_close(gen);
+  return 0;
+} /* }}} int format_gcm_resource */
+
+/* TypedValue
+ *
+ * {
+ *   // Union field, only one of the following:
+ *   "int64Value": string,
+ *   "doubleValue": number,
+ * }
+ */
+static int format_gcm_typed_value(yajl_gen gen, int ds_type,
+                                  value_t v) /* {{{ */
+{
+  char integer[21];
+
+  yajl_gen_map_open(gen);
+  if (ds_type == DS_TYPE_GAUGE) {
+    int status;
+
+    status = json_string(gen, "doubleValue");
+    if (status != 0)
+      return status;
+
+    status = (int)yajl_gen_double(gen, (double)v.gauge);
+    if (status != yajl_gen_status_ok)
+      return status;
+  } else {
+    switch (ds_type) {
+    case DS_TYPE_COUNTER:
+      snprintf(integer, sizeof(integer), "%llu", v.counter);
+      break;
+    case DS_TYPE_DERIVE:
+      snprintf(integer, sizeof(integer), "%" PRIi64, v.derive);
+      break;
+    case DS_TYPE_ABSOLUTE:
+      snprintf(integer, sizeof(integer), "%" PRIu64, v.derive);
+      break;
+    default:
+      ERROR("format_gcm_typed_value: unknown value type %d.", ds_type);
+      return EINVAL;
+    }
+
+    int status = json_string(gen, "int64Value") || json_string(gen, integer);
+    if (status != 0) {
+      return status;
+    }
+  }
+  yajl_gen_map_close(gen);
+
+  return 0;
+} /* }}} int format_gcm_typed_value */
+
+/* MetricKind
+ *
+ * enum(
+ *   "CUMULATIVE",
+ *   "GAUGE"
+ * )
+*/
+static int format_metric_kind(yajl_gen gen, int ds_type) {
+  return json_string(gen, (ds_type == DS_TYPE_GAUGE) ? "GAUGE" : "CUMULATIVE");
+}
+
+/* ValueType
+ *
+ * enum(
+ *   "DOUBLE",
+ *   "INT64"
+ * )
+*/
+static int format_value_type(yajl_gen gen, int ds_type) {
+  return json_string(gen, (ds_type == DS_TYPE_GAUGE) ? "DOUBLE" : "INT64");
+}
+
+static int metric_type(char *buffer, size_t buffer_size, data_set_t const *ds,
+                       value_list_t const *vl, int ds_index) {
+  /* {{{ */
+  char const *ds_name = ds->ds[ds_index].name;
+
+#define GCM_PREFIX "custom.googleapis.com/collectd/"
+  if ((ds_index != 0) || strcmp("value", ds_name) != 0) {
+    snprintf(buffer, buffer_size, GCM_PREFIX "%s/%s_%s", vl->plugin, vl->type,
+             ds_name);
+  } else {
+    snprintf(buffer, buffer_size, GCM_PREFIX "%s/%s", vl->plugin, vl->type);
+  }
+
+  char const *whitelist = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                          "abcdefghijklmnopqrstuvwxyz"
+                          "0123456789_/";
+  char *ptr = buffer + strlen(GCM_PREFIX);
+  size_t ok_len;
+  while ((ok_len = strspn(ptr, whitelist)) != strlen(ptr)) {
+    ptr[ok_len] = '_';
+    ptr += ok_len;
+  }
+
+  return 0;
+} /* }}} int metric_type */
+
+/* The metric type, including its DNS name prefix. The type is not URL-encoded.
+ * All user-defined custom metric types have the DNS name custom.googleapis.com.
+ * Metric types should use a natural hierarchical grouping. */
+static int format_metric_type(yajl_gen gen, data_set_t const *ds,
+                              value_list_t const *vl, int ds_index) {
+  /* {{{ */
+  char buffer[4 * DATA_MAX_NAME_LEN];
+  metric_type(buffer, sizeof(buffer), ds, vl, ds_index);
+
+  return json_string(gen, buffer);
+} /* }}} int format_metric_type */
+
+/* TimeInterval
+ *
+ * {
+ *   "endTime": string,
+ *   "startTime": string,
+ * }
+ */
+static int format_time_interval(yajl_gen gen, int ds_type,
+                                value_list_t const *vl) {
+  /* {{{ */
+  yajl_gen_map_open(gen);
+
+  int status = json_string(gen, "endTime") || json_time(gen, vl->time);
+  if (status != 0)
+    return status;
+
+  if (ds_type != DS_TYPE_GAUGE) {
+    cdtime_t start_time = 0;
+    if (uc_meta_data_get_unsigned_int(vl, "gcm:start_time", &start_time) != 0) {
+      start_time = vl->time;
+      uc_meta_data_add_unsigned_int(vl, "gcm:start_time", start_time);
+    }
+
+    int status = json_string(gen, "startTime") || json_time(gen, start_time);
+    if (status != 0)
+      return status;
+  }
+
+  yajl_gen_map_close(gen);
+  return 0;
+} /* }}} int format_time_interval */
+
+/* Point
+ *
+ * {
+ *   "interval": {
+ *     object(TimeInterval)
+ *   },
+ *   "value": {
+ *     object(TypedValue)
+ *   },
+ * }
+ */
+static int format_point(yajl_gen gen, data_set_t const *ds,
+                        value_list_t const *vl, int ds_index) {
+  /* {{{ */
+  yajl_gen_map_open(gen);
+
+  int ds_type = ds->ds[ds_index].type;
+
+  int status = json_string(gen, "interval") ||
+               format_time_interval(gen, ds_type, vl) ||
+               json_string(gen, "value") ||
+               format_gcm_typed_value(gen, ds_type, vl->values[ds_index]);
+  if (status != 0)
+    return status;
+
+  yajl_gen_map_close(gen);
+  return 0;
+} /* }}} int format_point */
+
+/* Metric
+ *
+ * {
+ *   "type": string,
+ *   "labels": {
+ *     string: string,
+ *     ...
+ *   },
+ * }
+ */
+static int format_metric(yajl_gen gen, data_set_t const *ds,
+                         value_list_t const *vl, int ds_index) {
+  /* {{{ */
+  yajl_gen_map_open(gen);
+
+  int status = json_string(gen, "type") ||
+               format_metric_type(gen, ds, vl, ds_index) ||
+               json_string(gen, "labels");
+  if (status != 0) {
+    return status;
+  }
+
+  yajl_gen_map_open(gen);
+  status = json_string(gen, "host") || json_string(gen, vl->host) ||
+           json_string(gen, "plugin_instance") ||
+           json_string(gen, vl->plugin_instance) ||
+           json_string(gen, "type_instance") ||
+           json_string(gen, vl->type_instance);
+  if (status != 0) {
+    return status;
+  }
+  yajl_gen_map_close(gen);
+
+  yajl_gen_map_close(gen);
+  return 0;
+} /* }}} int format_metric */
+
+/* TimeSeries
+ *
+ * {
+ *   "metric": {
+ *     object(Metric)
+ *   },
+ *   "resource": {
+ *     object(MonitoredResource)
+ *   },
+ *   "metricKind": enum(MetricKind),
+ *   "valueType": enum(ValueType),
+ *   "points": [
+ *     {
+ *       object(Point)
+ *     }
+ *   ],
+ * }
+ */
+static int format_time_series(yajl_gen gen, data_set_t const *ds,
+                              value_list_t const *vl, int ds_index,
+                              gcm_resource_t *res) {
+  /* {{{ */
+  yajl_gen_map_open(gen);
+
+  int ds_type = ds->ds[ds_index].type;
+
+  int status =
+      json_string(gen, "metric") || format_metric(gen, ds, vl, ds_index) ||
+      json_string(gen, "resource") || format_gcm_resource(gen, res) ||
+      json_string(gen, "metricKind") || format_metric_kind(gen, ds_type) ||
+      json_string(gen, "valueType") || format_value_type(gen, ds_type) ||
+      json_string(gen, "points");
+  if (status != 0)
+    return status;
+
+  yajl_gen_array_open(gen);
+  if ((status = format_point(gen, ds, vl, ds_index)) != 0) {
+    return status;
+  }
+  yajl_gen_array_close(gen);
+
+  yajl_gen_map_close(gen);
+  return 0;
+} /* }}} int format_time_series */
+
+/* Request body
+ *
+ * {
+ *   "timeSeries": [
+ *     {
+ *       object(TimeSeries)
+ *     }
+ *   ],
+ * }
+ */
+static int gcm_output_initialize(gcm_output_t *out) /* {{{ */
+{
+  yajl_gen_map_open(out->gen);
+
+  int status = json_string(out->gen, "timeSeries");
+  if (status != 0) {
+    return status;
+  }
+
+  yajl_gen_array_open(out->gen);
+  return 0;
+} /* }}} int gcm_output_initialize */
+
+static int gcm_output_finalize(gcm_output_t *out) /* {{{ */
+{
+  yajl_gen_array_close(out->gen);
+  yajl_gen_map_close(out->gen);
+
+  return 0;
+} /* }}} int gcm_output_finalize */
+
+static void gcm_output_reset_staged(gcm_output_t *out) /* {{{ */
+{
+  void *key = NULL;
+
+  while (c_avl_pick(out->staged, &key, &(void *){NULL}) == 0)
+    sfree(key);
+} /* }}} void gcm_output_reset_staged */
+
+gcm_output_t *gcm_output_create(gcm_resource_t *res) /* {{{ */
+{
+  gcm_output_t *out = calloc(1, sizeof(*out));
+  if (out == NULL)
+    return NULL;
+
+  out->res = res;
+
+  out->gen = yajl_gen_alloc(/* funcs = */ NULL);
+  if (out->gen == NULL) {
+    gcm_output_destroy(out);
+    return NULL;
+  }
+
+  out->staged = c_avl_create((void *)strcmp);
+  if (out->staged == NULL) {
+    gcm_output_destroy(out);
+    return NULL;
+  }
+
+  out->metric_descriptors = c_avl_create((void *)strcmp);
+  if (out->metric_descriptors == NULL) {
+    gcm_output_destroy(out);
+    return NULL;
+  }
+
+  gcm_output_initialize(out);
+
+  return out;
+} /* }}} gcm_output_t *gcm_output_create */
+
+void gcm_output_destroy(gcm_output_t *out) /* {{{ */
+{
+  if (out == NULL)
+    return;
+
+  if (out->metric_descriptors != NULL) {
+    void *key = NULL;
+    while (c_avl_pick(out->metric_descriptors, &key, &(void *){NULL}) == 0) {
+      sfree(key);
+    }
+    c_avl_destroy(out->metric_descriptors);
+    out->metric_descriptors = NULL;
+  }
+
+  if (out->staged != NULL) {
+    gcm_output_reset_staged(out);
+    c_avl_destroy(out->staged);
+    out->staged = NULL;
+  }
+
+  if (out->gen != NULL) {
+    yajl_gen_free(out->gen);
+    out->gen = NULL;
+  }
+
+  if (out->res != NULL) {
+    gcm_resource_destroy(out->res);
+    out->res = NULL;
+  }
+
+  sfree(out);
+} /* }}} void gcm_output_destroy */
+
+int gcm_output_add(gcm_output_t *out, data_set_t const *ds,
+                   value_list_t const *vl) /* {{{ */
+{
+  char key[6 * DATA_MAX_NAME_LEN];
+  int status;
+
+  /* first, check that we have all appropriate metric descriptors. */
+  for (size_t i = 0; i < ds->ds_num; i++) {
+    char buffer[4 * DATA_MAX_NAME_LEN];
+    metric_type(buffer, sizeof(buffer), ds, vl, i);
+
+    if (c_avl_get(out->metric_descriptors, buffer, NULL) != 0) {
+      return ENOENT;
+    }
+  }
+
+  status = FORMAT_VL(key, sizeof(key), vl);
+  if (status != 0) {
+    ERROR("gcm_output_add: FORMAT_VL failed with status %d.", status);
+    return status;
+  }
+
+  if (c_avl_get(out->staged, key, NULL) == 0) {
+    return EEXIST;
+  }
+
+  for (size_t i = 0; i < ds->ds_num; i++) {
+    int status = format_time_series(out->gen, ds, vl, i, out->res);
+    if (status != 0) {
+      ERROR("gcm_output_add: format_time_series failed with status %d.",
+            status);
+      return status;
+    }
+  }
+
+  c_avl_insert(out->staged, strdup(key), NULL);
+
+  size_t json_buffer_size = 0;
+  yajl_gen_get_buf(out->gen, &(unsigned char const *){NULL}, &json_buffer_size);
+  if (json_buffer_size > 65535)
+    return ENOBUFS;
+
+  return 0;
+} /* }}} int gcm_output_add */
+
+int gcm_output_register_metric(gcm_output_t *out, data_set_t const *ds,
+                               value_list_t const *vl) {
+  /* {{{ */
+  for (size_t i = 0; i < ds->ds_num; i++) {
+    char buffer[4 * DATA_MAX_NAME_LEN];
+    metric_type(buffer, sizeof(buffer), ds, vl, i);
+
+    char *key = strdup(buffer);
+    int status = c_avl_insert(out->metric_descriptors, key, NULL);
+    if (status != 0) {
+      sfree(key);
+      return status;
+    }
+  }
+
+  return 0;
+} /* }}} int gcm_output_register_metric */
+
+char *gcm_output_reset(gcm_output_t *out) /* {{{ */
+{
+  unsigned char const *json_buffer = NULL;
+  char *ret;
+
+  gcm_output_finalize(out);
+
+  yajl_gen_get_buf(out->gen, &json_buffer, &(size_t){0});
+  ret = strdup((void const *)json_buffer);
+
+  gcm_output_reset_staged(out);
+
+  yajl_gen_free(out->gen);
+  out->gen = yajl_gen_alloc(/* funcs = */ NULL);
+
+  gcm_output_initialize(out);
+
+  return ret;
+} /* }}} char *gcm_output_reset */
+
+gcm_resource_t *gcm_resource_create(char const *type) /* {{{ */
+{
+  gcm_resource_t *res;
+
+  res = malloc(sizeof(*res));
+  if (res == NULL)
+    return NULL;
+  memset(res, 0, sizeof(*res));
+
+  res->type = strdup(type);
+  if (res->type == NULL) {
+    sfree(res);
+    return NULL;
+  }
+
+  res->labels = NULL;
+  res->labels_num = 0;
+
+  return res;
+} /* }}} gcm_resource_t *gcm_resource_create */
+
+void gcm_resource_destroy(gcm_resource_t *res) /* {{{ */
+{
+  size_t i;
+
+  if (res == NULL)
+    return;
+
+  for (i = 0; i < res->labels_num; i++) {
+    sfree(res->labels[i].key);
+    sfree(res->labels[i].value);
+  }
+  sfree(res->labels);
+  sfree(res->type);
+  sfree(res);
+} /* }}} void gcm_resource_destroy */
+
+int gcm_resource_add_label(gcm_resource_t *res, char const *key,
+                           char const *value) /* {{{ */
+{
+  gcm_label_t *l;
+
+  if ((res == NULL) || (key == NULL) || (value == NULL))
+    return EINVAL;
+
+  l = realloc(res->labels, sizeof(*res->labels) * (res->labels_num + 1));
+  if (l == NULL)
+    return ENOMEM;
+
+  res->labels = l;
+  l = res->labels + res->labels_num;
+
+  l->key = strdup(key);
+  l->value = strdup(value);
+  if ((l->key == NULL) || (l->value == NULL)) {
+    sfree(l->key);
+    sfree(l->value);
+    return ENOMEM;
+  }
+
+  res->labels_num++;
+  return 0;
+} /* }}} int gcm_resource_add_label */
+
+/* LabelDescriptor
+ *
+ * {
+ *   "key": string,
+ *   "valueType": enum(ValueType),
+ *   "description": string,
+ * }
+ */
+static int format_label_descriptor(yajl_gen gen, char const *key) {
+  /* {{{ */
+  yajl_gen_map_open(gen);
+
+  int status = json_string(gen, "key") || json_string(gen, key) ||
+               json_string(gen, "valueType") || json_string(gen, "STRING");
+  if (status != 0) {
+    return status;
+  }
+
+  yajl_gen_map_close(gen);
+  return 0;
+} /* }}} int format_label_descriptor */
+
+/* MetricDescriptor
+ *
+ * {
+ *   "name": string,
+ *   "type": string,
+ *   "labels": [
+ *     {
+ *       object(LabelDescriptor)
+ *     }
+ *   ],
+ *   "metricKind": enum(MetricKind),
+ *   "valueType": enum(ValueType),
+ *   "unit": string,
+ *   "description": string,
+ *   "displayName": string,
+ * }
+ */
+int gcm_format_metric_descriptor(char *buffer, size_t buffer_size,
+                                 data_set_t const *ds, value_list_t const *vl,
+                                 int ds_index) {
+  /* {{{ */
+  yajl_gen gen = yajl_gen_alloc(/* funcs = */ NULL);
+  if (gen == NULL) {
+    return ENOMEM;
+  }
+
+  int ds_type = ds->ds[ds_index].type;
+
+  yajl_gen_map_open(gen);
+
+  int status =
+      json_string(gen, "type") || format_metric_type(gen, ds, vl, ds_index) ||
+      json_string(gen, "metricKind") || format_metric_kind(gen, ds_type) ||
+      json_string(gen, "valueType") || format_value_type(gen, ds_type) ||
+      json_string(gen, "labels");
+  if (status != 0) {
+    yajl_gen_free(gen);
+    return status;
+  }
+
+  char const *labels[] = {"host", "plugin_instance", "type_instance"};
+  yajl_gen_array_open(gen);
+
+  for (size_t i = 0; i < STATIC_ARRAY_SIZE(labels); i++) {
+    int status = format_label_descriptor(gen, labels[i]);
+    if (status != 0) {
+      yajl_gen_free(gen);
+      return status;
+    }
+  }
+
+  yajl_gen_array_close(gen);
+  yajl_gen_map_close(gen);
+
+  unsigned char const *tmp = NULL;
+  yajl_gen_get_buf(gen, &tmp, &(size_t){0});
+  sstrncpy(buffer, (void const *)tmp, buffer_size);
+
+  yajl_gen_free(gen);
+  return 0;
+} /* }}} int gcm_format_metric_descriptor */
+
+/* vim: set sw=2 sts=2 et fdm=marker : */
diff --git a/src/utils_format_gcm.h b/src/utils_format_gcm.h
new file mode 100644 (file)
index 0000000..a43812c
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * collectd - src/utils_format_gcm.h
+ * ISC license
+ *
+ * Copyright (C) 2017  Florian Forster
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ * Authors:
+ *   Florian Forster <octo at collectd.org>
+ **/
+
+#ifndef UTILS_FORMAT_GCM_H
+#define UTILS_FORMAT_GCM_H 1
+
+#include "collectd.h"
+#include "plugin.h"
+
+/* gcm_output_t is a buffer to which value_list_t* can be added and from which
+ * an appropriately formatted char* can be read. */
+struct gcm_output_s;
+typedef struct gcm_output_s gcm_output_t;
+
+/* gcm_resource_t represents a MonitoredResource. */
+struct gcm_resource_s;
+typedef struct gcm_resource_s gcm_resource_t;
+
+gcm_output_t *gcm_output_create(gcm_resource_t *res);
+
+/* gcm_output_destroy frees all memory used by out, including the
+ * gcm_resource_t* passed to gcm_output_create. */
+void gcm_output_destroy(gcm_output_t *out);
+
+/* gcm_output_add adds a value_list_t* to "out".
+ *
+ * Return values:
+ *   - 0        Success
+ *   - ENOBUFS  Success, but the buffer should be flushed soon.
+ *   - EEXIST   The value list is already encoded in the buffer.
+ *              Flush the buffer, then call gcm_output_add again.
+ *   - ENOENT   First time we encounter this metric. Create a metric descriptor
+ *              using the GCM API and then call gcm_output_register_metric.
+ */
+int gcm_output_add(gcm_output_t *out, data_set_t const *ds,
+                   value_list_t const *vl);
+
+/* gcm_output_register_metric adds the metric descriptor which vl maps to, to
+ * the list of known metric descriptors. */
+int gcm_output_register_metric(gcm_output_t *out, data_set_t const *ds,
+                               value_list_t const *vl);
+
+/* gcm_output_reset resets the output and returns the previous content of the
+ * buffer. It is the caller's responsibility to call free() with the returned
+ * pointer. */
+char *gcm_output_reset(gcm_output_t *out);
+
+gcm_resource_t *gcm_resource_create(char const *type);
+void gcm_resource_destroy(gcm_resource_t *res);
+int gcm_resource_add_label(gcm_resource_t *res, char const *key,
+                           char const *value);
+
+/* gcm_format_metric_descriptor creates the payload for a
+ * projects.metricDescriptors.create() request. */
+int gcm_format_metric_descriptor(char *buffer, size_t buffer_size,
+                                 data_set_t const *ds, value_list_t const *vl,
+                                 int ds_index);
+
+#endif /* UTILS_FORMAT_GCM_H */
diff --git a/src/utils_format_gcm_test.c b/src/utils_format_gcm_test.c
new file mode 100644 (file)
index 0000000..52d6dc9
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * collectd - src/utils_format_gcm_test.c
+ * ISC license
+ *
+ * Copyright (C) 2017  Florian Forster
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ * Authors:
+ *   Florian Forster <octo at collectd.org>
+ **/
+
+#include "utils_format_gcm.h"
+#include "testing.h"
+
+DEF_TEST(gcm_format_metric_descriptor) {
+  value_list_t vl = {
+      .host = "example.com", .plugin = "unit-test", .type = "example",
+  };
+  char got[1024];
+
+  data_set_t ds_single = {
+      .type = "example",
+      .ds_num = 1,
+      .ds =
+          &(data_source_t){
+              .name = "value", .type = DS_TYPE_GAUGE, .min = NAN, .max = NAN,
+          },
+  };
+  EXPECT_EQ_INT(
+      0, gcm_format_metric_descriptor(got, sizeof(got), &ds_single, &vl, 0));
+  char const *want_single =
+      "{\"type\":\"custom.googleapis.com/collectd/unit_test/"
+      "example\",\"metricKind\":\"GAUGE\",\"valueType\":\"DOUBLE\",\"labels\":["
+      "{\"key\":\"host\",\"valueType\":\"STRING\"},{\"key\":\"plugin_"
+      "instance\",\"valueType\":\"STRING\"},{\"key\":\"type_instance\","
+      "\"valueType\":\"STRING\"}]}";
+  EXPECT_EQ_STR(want_single, got);
+
+  data_set_t ds_double = {
+      .type = "example",
+      .ds_num = 2,
+      .ds =
+          (data_source_t[]){
+              {.name = "one", .type = DS_TYPE_DERIVE, .min = 0, .max = NAN},
+              {.name = "two", .type = DS_TYPE_DERIVE, .min = 0, .max = NAN},
+          },
+  };
+  EXPECT_EQ_INT(
+      0, gcm_format_metric_descriptor(got, sizeof(got), &ds_double, &vl, 0));
+  char const *want_double =
+      "{\"type\":\"custom.googleapis.com/collectd/unit_test/"
+      "example_one\",\"metricKind\":\"CUMULATIVE\",\"valueType\":\"INT64\","
+      "\"labels\":[{\"key\":\"host\",\"valueType\":\"STRING\"},{\"key\":"
+      "\"plugin_instance\",\"valueType\":\"STRING\"},{\"key\":\"type_"
+      "instance\",\"valueType\":\"STRING\"}]}";
+  EXPECT_EQ_STR(want_double, got);
+  return 0;
+}
+
+int main(int argc, char **argv) {
+  RUN_TEST(gcm_format_metric_descriptor);
+
+  END_TEST;
+}