Rename write_gcm to write_stackdriver.
authorFlorian Forster <octo@collectd.org>
Mon, 6 Nov 2017 20:26:08 +0000 (21:26 +0100)
committerFlorian Forster <octo@collectd.org>
Thu, 4 Oct 2018 19:14:37 +0000 (21:14 +0200)
12 files changed:
Makefile.am
configure.ac
src/collectd.conf.in
src/collectd.conf.pod
src/utils_format_gcm.c [deleted file]
src/utils_format_gcm.h [deleted file]
src/utils_format_gcm_test.c [deleted file]
src/utils_format_stackdriver.c [new file with mode: 0644]
src/utils_format_stackdriver.h [new file with mode: 0644]
src/utils_format_stackdriver_test.c [new file with mode: 0644]
src/write_gcm.c [deleted file]
src/write_stackdriver.c [new file with mode: 0644]

index 93a4ca8..266ff10 100644 (file)
@@ -616,25 +616,25 @@ libgce_la_LIBADD = \
 endif
 
 if BUILD_WITH_LIBYAJL2
-noinst_LTLIBRARIES += libformat_gcm.la
-libformat_gcm_la_SOURCES = \
-       src/utils_format_gcm.c \
-       src/utils_format_gcm.h
-libformat_gcm_la_CPPFLAGS = \
+noinst_LTLIBRARIES += libformat_stackdriver.la
+libformat_stackdriver_la_SOURCES = \
+       src/utils_format_stackdriver.c \
+       src/utils_format_stackdriver.h
+libformat_stackdriver_la_CPPFLAGS = \
        $(AM_CPPFLAGS) \
        $(BUILD_WITH_LIBYAJL_CPPFLAGS)
-libformat_gcm_la_LIBADD = \
+libformat_stackdriver_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 = \
-       src/utils_format_gcm_test.c \
+check_PROGRAMS += test_format_stackdriver
+TESTS += test_format_stackdriver
+test_format_stackdriver_SOURCES = \
+       src/utils_format_stackdriver_test.c \
        src/testing.h
-test_format_gcm_LDADD = \
-       libformat_gcm.la \
+test_format_stackdriver_LDADD = \
+       libformat_stackdriver.la \
        libplugin_mock.la \
        -lm
 endif
@@ -1980,15 +1980,6 @@ write_http_la_LDFLAGS = $(PLUGIN_LDFLAGS)
 write_http_la_LIBADD = libformat_json.la $(BUILD_WITH_LIBCURL_LIBS)
 endif
 
-if BUILD_PLUGIN_WRITE_GCM
-pkglib_LTLIBRARIES += write_gcm.la
-write_gcm_la_SOURCES = src/write_gcm.c
-write_gcm_la_LDFLAGS = $(PLUGIN_LDFLAGS)
-write_gcm_la_CPPFLAGS = $(AM_CPPFLAGS) $(BUILD_WITH_LIBCURL_CFLAGS)
-write_gcm_la_LIBADD = libformat_gcm.la libgce.la liboauth.la \
-                     $(BUILD_WITH_LIBCURL_LIBS)
-endif
-
 if BUILD_PLUGIN_WRITE_KAFKA
 pkglib_LTLIBRARIES += write_kafka.la
 write_kafka_la_SOURCES = src/write_kafka.c
@@ -2050,6 +2041,15 @@ write_sensu_la_SOURCES = src/write_sensu.c
 write_sensu_la_LDFLAGS = $(PLUGIN_LDFLAGS)
 endif
 
+if BUILD_PLUGIN_WRITE_STACKDRIVER
+pkglib_LTLIBRARIES += write_stackdriver.la
+write_stackdriver_la_SOURCES = src/write_stackdriver.c
+write_stackdriver_la_LDFLAGS = $(PLUGIN_LDFLAGS)
+write_stackdriver_la_CPPFLAGS = $(AM_CPPFLAGS) $(BUILD_WITH_LIBCURL_CFLAGS)
+write_stackdriver_la_LIBADD = libformat_stackdriver.la libgce.la liboauth.la \
+                     $(BUILD_WITH_LIBCURL_LIBS)
+endif
+
 if BUILD_PLUGIN_WRITE_TSDB
 pkglib_LTLIBRARIES += write_tsdb.la
 write_tsdb_la_SOURCES = src/write_tsdb.c
index 92c81ee..5333c42 100644 (file)
@@ -6565,7 +6565,7 @@ if test "x$with_libcurl" = "xyes" && test "x$with_libyajl" = "xyes"; then
 fi
 
 if test "x$with_libcurl" = "xyes" && test "x$with_libssl" = "xyes" && test "x$with_libyajl" = "xyes" && test "x$with_libyajl2" = "xyes"; then
-  plugin_write_gcm="yes"
+  plugin_write_stackdriver="yes"
 fi
 
 if test "x$with_libcurl" = "xyes" && test "x$with_libxml2" = "xyes"; then
@@ -6900,7 +6900,7 @@ AC_PLUGIN([vserver],             [$plugin_vserver],         [Linux VServer stati
 AC_PLUGIN([wireless],            [$plugin_wireless],        [Wireless statistics])
 AC_PLUGIN([write_graphite],      [yes],                     [Graphite / Carbon output plugin])
 AC_PLUGIN([write_http],          [$with_libcurl],           [HTTP output plugin])
-AC_PLUGIN([write_gcm],           [$plugin_write_gcm],  [Google cloud monitoring output plugin])
+AC_PLUGIN([write_stackdriver],   [$plugin_write_stackdriver], [Google Stackdriver Monitoring output plugin])
 AC_PLUGIN([write_kafka],         [$with_librdkafka],        [Kafka output plugin])
 AC_PLUGIN([write_log],           [yes],                     [Log output plugin])
 AC_PLUGIN([write_mongodb],       [$with_libmongoc],         [MongoDB output plugin])
@@ -7323,7 +7323,6 @@ AC_MSG_RESULT([    vserver . . . . . . . $enable_vserver])
 AC_MSG_RESULT([    wireless  . . . . . . $enable_wireless])
 AC_MSG_RESULT([    write_graphite  . . . $enable_write_graphite])
 AC_MSG_RESULT([    write_http  . . . . . $enable_write_http])
-AC_MSG_RESULT([    write_gcm . . . . . . $enable_write_gcm])
 AC_MSG_RESULT([    write_kafka . . . . . $enable_write_kafka])
 AC_MSG_RESULT([    write_log . . . . . . $enable_write_log])
 AC_MSG_RESULT([    write_mongodb . . . . $enable_write_mongodb])
@@ -7331,6 +7330,7 @@ AC_MSG_RESULT([    write_prometheus. . . $enable_write_prometheus])
 AC_MSG_RESULT([    write_redis . . . . . $enable_write_redis])
 AC_MSG_RESULT([    write_riemann . . . . $enable_write_riemann])
 AC_MSG_RESULT([    write_sensu . . . . . $enable_write_sensu])
+AC_MSG_RESULT([    write_stackdriver . . $enable_write_stackdriver])
 AC_MSG_RESULT([    write_tsdb  . . . . . $enable_write_tsdb])
 AC_MSG_RESULT([    xencpu  . . . . . . . $enable_xencpu])
 AC_MSG_RESULT([    xmms  . . . . . . . . $enable_xmms])
index 6a09643..184d0b9 100644 (file)
 #@BUILD_PLUGIN_VMEM_TRUE@LoadPlugin vmem
 #@BUILD_PLUGIN_VSERVER_TRUE@LoadPlugin vserver
 #@BUILD_PLUGIN_WIRELESS_TRUE@LoadPlugin wireless
-#@BUILD_PLUGIN_WRITE_GCM_TRUE@LoadPlugin write_gcm
 #@BUILD_PLUGIN_WRITE_GRAPHITE_TRUE@LoadPlugin write_graphite
 #@BUILD_PLUGIN_WRITE_HTTP_TRUE@LoadPlugin write_http
 #@BUILD_PLUGIN_WRITE_KAFKA_TRUE@LoadPlugin write_kafka
 #@BUILD_PLUGIN_WRITE_REDIS_TRUE@LoadPlugin write_redis
 #@BUILD_PLUGIN_WRITE_RIEMANN_TRUE@LoadPlugin write_riemann
 #@BUILD_PLUGIN_WRITE_SENSU_TRUE@LoadPlugin write_sensu
+#@BUILD_PLUGIN_WRITE_STACKDRIVER_TRUE@LoadPlugin write_stackdriver
 #@BUILD_PLUGIN_WRITE_TSDB_TRUE@LoadPlugin write_tsdb
 #@BUILD_PLUGIN_XENCPU_TRUE@LoadPlugin xencpu
 #@BUILD_PLUGIN_XMMS_TRUE@LoadPlugin xmms
 #      Verbose false
 #</Plugin>
 
-#<Plugin write_gcm>
-#  Project "stackdriver-account"
-#  CredentialFile "/path/to/gcp-project-id-12345.json"
-#  Email "123456789012@developer.gserviceaccount.com"
-#  <Resource "global">
-#    project_id "gcp-project-id"
-#  </Resource>
-#  Url "https://monitoring.googleapis.com/v3"
-#</Plugin>
-
 #<Plugin write_graphite>
 #  <Node "example">
 #    Host "localhost"
 #      Attribute "foo" "bar"
 #</Plugin>
 
+#<Plugin write_stackdriver>
+#  Project "stackdriver-account"
+#  CredentialFile "/path/to/gcp-project-id-12345.json"
+#  Email "123456789012@developer.gserviceaccount.com"
+#  <Resource "global">
+#    project_id "gcp-project-id"
+#  </Resource>
+#  Url "https://monitoring.googleapis.com/v3"
+#</Plugin>
+
 #<Plugin write_tsdb>
 #      <Node>
 #              Host "localhost"
index e293d49..7ff36b2 100644 (file)
@@ -9337,86 +9337,6 @@ traffic (e.E<nbsp>g. due to headers and retransmission). If you want to
 collect on-wire traffic you could, for example, use the logging facilities of
 iptables to feed data for the guest IPs into the iptables plugin.
 
-=head2 Plugin C<write_gcm>
-
-The C<write_gcm> plugin writes metrics to the I<Google Cloud Monitoring> (GCM)
-service.
-
-This plugin supports two authentication methods: When configured, credentials
-are read from the JSON credentials file specified with B<CredentialFile>.
-Alternatively, when running on
-I<Google Compute Engine> (GCE), an I<OAuth> token is retrieved from the
-I<metadata server> and used to authenticate to GCM.
-
-B<Synopsis:>
-
- <Plugin write_gcm>
-   CredentialFile "/path/to/service_account.json"
-   <Resource "global">
-     project_id "monitored_project"
-   </Resource>
- </Plugin>
-
-=over 4
-
-=item B<CredentialFile> I<file>
-
-Path to a JSON credentials file holding the credentials for a GCP service
-account.
-
-If not specified, I<Application Default Credentials>. If running on GCE,
-B<Email> may be set to chose a different service account associated with the
-instance.
-
-=item B<Project> I<Project>
-
-The I<Project ID> or the I<Project Number> of the I<Stackdriver Account>. The
-I<Project ID> is a string identifying the GCP project, which you can chose
-freely when creating a new project. The I<Project Number> is a 12-digit decimal
-number. You can look up both on the I<Developer Console>.
-
-This setting is optional. If not set, the project ID is read from the
-credentials file or determined from the GCE's metadata service.
-
-=item B<Email> I<Email>
-
-Email address of an GCE I<Service Account>. This setting is only effective when
-running on GCE and using I<Application Default Credentials> (see
-B<CredentialFile> above).
-
-=item B<Resource> I<ResourceType>
-
-Configures the I<Monitored Resource> to use when storing metrics. This option
-takes a I<ResourceType> and arbitrary string options which are used as labels.
-
-On GCE, defaults to the equivalent of this config:
-
-  <Resource "gce_instance">
-    project_id "${meta/project/project-id}"
-    instance_id "${meta/instance/id}"
-    zone "${meta/instance/zone}"
-  </Resource>
-
-Where C<${meta/...}> are values read from the meta data service.
-
-When not running on GCE, defaults to the equivalent of this config:
-
-  <Resource "global">
-    project_id "${Project}"
-  </Resource>
-
-Where C<${Project}> refers to the B<Project> option.
-
-See L<https://cloud.google.com/monitoring/api/resources> for more information
-on I<Monitored Resource Types>.
-
-=item B<Url> I<Url>
-
-URL of the I<Google Cloud Monitoring> API. Defaults to
-C<https://monitoring.googleapis.com/v3>.
-
-=back
-
 =head2 Plugin C<write_graphite>
 
 The C<write_graphite> plugin writes data to I<Graphite>, an open-source metrics
@@ -10387,6 +10307,86 @@ attribute for each metric being sent out to I<Sensu>.
 
 =back
 
+=head2 Plugin C<write_stackdriver>
+
+The C<write_stackdriver> plugin writes metrics to the
+I<Google Stackdriver Monitoring> service.
+
+This plugin supports two authentication methods: When configured, credentials
+are read from the JSON credentials file specified with B<CredentialFile>.
+Alternatively, when running on
+I<Google Compute Engine> (GCE), an I<OAuth> token is retrieved from the
+I<metadata server> and used to authenticate to GCM.
+
+B<Synopsis:>
+
+ <Plugin write_stackdriver>
+   CredentialFile "/path/to/service_account.json"
+   <Resource "global">
+     project_id "monitored_project"
+   </Resource>
+ </Plugin>
+
+=over 4
+
+=item B<CredentialFile> I<file>
+
+Path to a JSON credentials file holding the credentials for a GCP service
+account.
+
+If not specified, I<Application Default Credentials>. If running on GCE,
+B<Email> may be set to chose a different service account associated with the
+instance.
+
+=item B<Project> I<Project>
+
+The I<Project ID> or the I<Project Number> of the I<Stackdriver Account>. The
+I<Project ID> is a string identifying the GCP project, which you can chose
+freely when creating a new project. The I<Project Number> is a 12-digit decimal
+number. You can look up both on the I<Developer Console>.
+
+This setting is optional. If not set, the project ID is read from the
+credentials file or determined from the GCE's metadata service.
+
+=item B<Email> I<Email>
+
+Email address of an GCE I<Service Account>. This setting is only effective when
+running on GCE and using I<Application Default Credentials> (see
+B<CredentialFile> above).
+
+=item B<Resource> I<ResourceType>
+
+Configures the I<Monitored Resource> to use when storing metrics. This option
+takes a I<ResourceType> and arbitrary string options which are used as labels.
+
+On GCE, defaults to the equivalent of this config:
+
+  <Resource "gce_instance">
+    project_id "${meta/project/project-id}"
+    instance_id "${meta/instance/id}"
+    zone "${meta/instance/zone}"
+  </Resource>
+
+Where C<${meta/...}> are values read from the meta data service.
+
+When not running on GCE, defaults to the equivalent of this config:
+
+  <Resource "global">
+    project_id "${Project}"
+  </Resource>
+
+Where C<${Project}> refers to the B<Project> option.
+
+See L<https://cloud.google.com/monitoring/api/resources> for more information
+on I<Monitored Resource Types>.
+
+=item B<Url> I<Url>
+
+URL of the I<Google Cloud Monitoring> API. Defaults to
+C<https://monitoring.googleapis.com/v3>.
+
+=back
+
 =head2 Plugin C<xencpu>
 
 This plugin collects metrics of hardware CPU load for machine running Xen
diff --git a/src/utils_format_gcm.c b/src/utils_format_gcm.c
deleted file mode 100644 (file)
index 0c60ef0..0000000
+++ /dev/null
@@ -1,710 +0,0 @@
-/**
- * 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
deleted file mode 100644 (file)
index a43812c..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * 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
deleted file mode 100644 (file)
index 52d6dc9..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * 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;
-}
diff --git a/src/utils_format_stackdriver.c b/src/utils_format_stackdriver.c
new file mode 100644 (file)
index 0000000..fda049c
--- /dev/null
@@ -0,0 +1,709 @@
+/**
+ * collectd - src/utils_format_stackdriver.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_stackdriver.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 sd_output_s {
+  sd_resource_t *res;
+  yajl_gen gen;
+  c_avl_tree_t *staged;
+  c_avl_tree_t *metric_descriptors;
+};
+
+struct sd_label_s {
+  char *key;
+  char *value;
+};
+typedef struct sd_label_s sd_label_t;
+
+struct sd_resource_s {
+  char *type;
+
+  sd_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, sd_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,
+                              sd_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 sd_output_initialize(sd_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 sd_output_initialize */
+
+static int sd_output_finalize(sd_output_t *out) /* {{{ */
+{
+  yajl_gen_array_close(out->gen);
+  yajl_gen_map_close(out->gen);
+
+  return 0;
+} /* }}} int sd_output_finalize */
+
+static void sd_output_reset_staged(sd_output_t *out) /* {{{ */
+{
+  void *key = NULL;
+
+  while (c_avl_pick(out->staged, &key, &(void *){NULL}) == 0)
+    sfree(key);
+} /* }}} void sd_output_reset_staged */
+
+sd_output_t *sd_output_create(sd_resource_t *res) /* {{{ */
+{
+  sd_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) {
+    sd_output_destroy(out);
+    return NULL;
+  }
+
+  out->staged = c_avl_create((void *)strcmp);
+  if (out->staged == NULL) {
+    sd_output_destroy(out);
+    return NULL;
+  }
+
+  out->metric_descriptors = c_avl_create((void *)strcmp);
+  if (out->metric_descriptors == NULL) {
+    sd_output_destroy(out);
+    return NULL;
+  }
+
+  sd_output_initialize(out);
+
+  return out;
+} /* }}} sd_output_t *sd_output_create */
+
+void sd_output_destroy(sd_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) {
+    sd_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) {
+    sd_resource_destroy(out->res);
+    out->res = NULL;
+  }
+
+  sfree(out);
+} /* }}} void sd_output_destroy */
+
+int sd_output_add(sd_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("sd_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("sd_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 sd_output_add */
+
+int sd_output_register_metric(sd_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 sd_output_register_metric */
+
+char *sd_output_reset(sd_output_t *out) /* {{{ */
+{
+  unsigned char const *json_buffer = NULL;
+  char *ret;
+
+  sd_output_finalize(out);
+
+  yajl_gen_get_buf(out->gen, &json_buffer, &(size_t){0});
+  ret = strdup((void const *)json_buffer);
+
+  sd_output_reset_staged(out);
+
+  yajl_gen_free(out->gen);
+  out->gen = yajl_gen_alloc(/* funcs = */ NULL);
+
+  sd_output_initialize(out);
+
+  return ret;
+} /* }}} char *sd_output_reset */
+
+sd_resource_t *sd_resource_create(char const *type) /* {{{ */
+{
+  sd_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;
+} /* }}} sd_resource_t *sd_resource_create */
+
+void sd_resource_destroy(sd_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 sd_resource_destroy */
+
+int sd_resource_add_label(sd_resource_t *res, char const *key,
+                          char const *value) /* {{{ */
+{
+  sd_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 sd_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 sd_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 sd_format_metric_descriptor */
+
+/* vim: set sw=2 sts=2 et fdm=marker : */
diff --git a/src/utils_format_stackdriver.h b/src/utils_format_stackdriver.h
new file mode 100644 (file)
index 0000000..fee260e
--- /dev/null
@@ -0,0 +1,79 @@
+/**
+ * collectd - src/utils_format_stackdriver.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_STACKDRIVER_H
+#define UTILS_FORMAT_STACKDRIVER_H 1
+
+#include "collectd.h"
+#include "plugin.h"
+
+/* sd_output_t is a buffer to which value_list_t* can be added and from which
+ * an appropriately formatted char* can be read. */
+struct sd_output_s;
+typedef struct sd_output_s sd_output_t;
+
+/* sd_resource_t represents a MonitoredResource. */
+struct sd_resource_s;
+typedef struct sd_resource_s sd_resource_t;
+
+sd_output_t *sd_output_create(sd_resource_t *res);
+
+/* sd_output_destroy frees all memory used by out, including the
+ * sd_resource_t* passed to sd_output_create. */
+void sd_output_destroy(sd_output_t *out);
+
+/* sd_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 sd_output_add again.
+ *   - ENOENT   First time we encounter this metric. Create a metric descriptor
+ *              using the Stackdriver API and then call
+ *              sd_output_register_metric.
+ */
+int sd_output_add(sd_output_t *out, data_set_t const *ds,
+                  value_list_t const *vl);
+
+/* sd_output_register_metric adds the metric descriptor which vl maps to, to
+ * the list of known metric descriptors. */
+int sd_output_register_metric(sd_output_t *out, data_set_t const *ds,
+                              value_list_t const *vl);
+
+/* sd_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 *sd_output_reset(sd_output_t *out);
+
+sd_resource_t *sd_resource_create(char const *type);
+void sd_resource_destroy(sd_resource_t *res);
+int sd_resource_add_label(sd_resource_t *res, char const *key,
+                          char const *value);
+
+/* sd_format_metric_descriptor creates the payload for a
+ * projects.metricDescriptors.create() request. */
+int sd_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_STACKDRIVER_H */
diff --git a/src/utils_format_stackdriver_test.c b/src/utils_format_stackdriver_test.c
new file mode 100644 (file)
index 0000000..fa43866
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * collectd - src/utils_format_stackdriver_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 "testing.h"
+#include "utils_format_stackdriver.h"
+
+DEF_TEST(sd_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, sd_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, sd_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(sd_format_metric_descriptor);
+
+  END_TEST;
+}
diff --git a/src/write_gcm.c b/src/write_gcm.c
deleted file mode 100644 (file)
index 8ab1eec..0000000
+++ /dev/null
@@ -1,637 +0,0 @@
-/**
- * collectd - src/write_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 "common.h"
-#include "configfile.h"
-#include "plugin.h"
-#include "utils_format_gcm.h"
-#include "utils_gce.h"
-#include "utils_oauth.h"
-
-#include <curl/curl.h>
-#include <pthread.h>
-
-/*
- * Private variables
- */
-#ifndef GCM_API_URL
-#define GCM_API_URL "https://monitoring.googleapis.com/v3"
-#endif
-
-#ifndef MONITORING_SCOPE
-#define MONITORING_SCOPE "https://www.googleapis.com/auth/monitoring"
-#endif
-
-struct wg_callback_s {
-  /* config */
-  char *email;
-  char *project;
-  char *url;
-  gcm_resource_t *resource;
-
-  /* runtime */
-  oauth_t *auth;
-  gcm_output_t *formatter;
-  CURL *curl;
-  char curl_errbuf[CURL_ERROR_SIZE];
-  /* used by flush */
-  size_t timeseries_count;
-  cdtime_t send_buffer_init_time;
-
-  pthread_mutex_t lock;
-};
-typedef struct wg_callback_s wg_callback_t;
-
-struct wg_memory_s {
-  char *memory;
-  size_t size;
-};
-typedef struct wg_memory_s wg_memory_t;
-
-static size_t wg_write_memory_cb(void *contents, size_t size,
-                                 size_t nmemb, /* {{{ */
-                                 void *userp) {
-  size_t realsize = size * nmemb;
-  wg_memory_t *mem = (wg_memory_t *)userp;
-
-  if (0x7FFFFFF0 < mem->size || 0x7FFFFFF0 - mem->size < realsize) {
-    ERROR("integer overflow");
-    return 0;
-  }
-
-  mem->memory = (char *)realloc((void *)mem->memory, mem->size + realsize + 1);
-  if (mem->memory == NULL) {
-    /* out of memory! */
-    ERROR("wg_write_memory_cb: not enough memory (realloc returned NULL)");
-    return 0;
-  }
-
-  memcpy(&(mem->memory[mem->size]), contents, realsize);
-  mem->size += realsize;
-  mem->memory[mem->size] = 0;
-  return realsize;
-} /* }}} size_t wg_write_memory_cb */
-
-static char *wg_get_authorization_header(wg_callback_t *cb) { /* {{{ */
-  int status = 0;
-  char access_token[256];
-  char authorization_header[256];
-
-  assert((cb->auth != NULL) || gce_check());
-  if (cb->auth != NULL)
-    status = oauth_access_token(cb->auth, access_token, sizeof(access_token));
-  else
-    status = gce_access_token(cb->email, access_token, sizeof(access_token));
-  if (status != 0) {
-    ERROR("write_gcm plugin: Failed to get access token");
-    return NULL;
-  }
-
-  status = snprintf(authorization_header, sizeof(authorization_header),
-                    "Authorization: Bearer %s", access_token);
-  if ((status < 1) || ((size_t)status >= sizeof(authorization_header)))
-    return NULL;
-
-  return strdup(authorization_header);
-} /* }}} char *wg_get_authorization_header */
-
-static int wg_call_metricdescriptor_create(wg_callback_t *cb,
-                                           char const *payload) {
-  /* {{{ */
-  char final_url[1024];
-  int status =
-      snprintf(final_url, sizeof(final_url), "%s/projects/%s/metricDescriptors",
-               cb->url, cb->project);
-  if ((status < 1) || ((size_t)status >= sizeof(final_url)))
-    return -1;
-
-  char *authorization_header = wg_get_authorization_header(cb);
-  if (authorization_header == NULL)
-    return -1;
-
-  struct curl_slist *headers = NULL;
-  headers = curl_slist_append(headers, "Content-Type: application/json");
-  headers = curl_slist_append(headers, authorization_header);
-
-  CURL *curl = curl_easy_init();
-  if (!curl) {
-    ERROR("write_gcm plugin: curl_easy_init failed.");
-    curl_slist_free_all(headers);
-    sfree(authorization_header);
-    return -1;
-  }
-
-  curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
-  curl_easy_setopt(cb->curl, CURLOPT_USERAGENT,
-                   PACKAGE_NAME "/" PACKAGE_VERSION);
-  char curl_errbuf[CURL_ERROR_SIZE];
-  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
-  curl_easy_setopt(curl, CURLOPT_URL, final_url);
-  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
-  curl_easy_setopt(curl, CURLOPT_POST, 1L);
-  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
-
-  wg_memory_t res = {
-      .memory = NULL, .size = 0,
-  };
-  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, wg_write_memory_cb);
-  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res);
-
-  status = curl_easy_perform(curl);
-  if (status != CURLE_OK) {
-    ERROR("write_gcm plugin: curl_easy_perform failed with status %d: %s",
-          status, curl_errbuf);
-    sfree(res.memory);
-    curl_easy_cleanup(curl);
-    curl_slist_free_all(headers);
-    sfree(authorization_header);
-    return -1;
-  }
-
-  long http_code = 0;
-  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
-  if ((http_code < 200) || (http_code >= 300)) {
-    ERROR("write_gcm plugin: POST request to %s failed: HTTP error %ld",
-          final_url, http_code);
-    INFO("write_gcm plugin: Server replied: %s", res.memory);
-    sfree(res.memory);
-    curl_easy_cleanup(curl);
-    curl_slist_free_all(headers);
-    sfree(authorization_header);
-    return -1;
-  }
-
-  sfree(res.memory);
-  curl_easy_cleanup(curl);
-  curl_slist_free_all(headers);
-  sfree(authorization_header);
-  return 0;
-} /* }}} int wg_call_metricdescriptor_create */
-
-static void wg_reset_buffer(wg_callback_t *cb) /* {{{ */
-{
-  cb->timeseries_count = 0;
-  cb->send_buffer_init_time = cdtime();
-} /* }}} wg_reset_buffer */
-
-static int wg_call_timeseries_write(wg_callback_t *cb,
-                                    char const *payload) /* {{{ */
-{
-  char final_url[1024];
-  int status = snprintf(final_url, sizeof(final_url),
-                        "%s/projects/%s/timeSeries", cb->url, cb->project);
-  if ((status < 1) || ((size_t)status >= sizeof(final_url)))
-    return -1;
-
-  char *authorization_header = wg_get_authorization_header(cb);
-  if (authorization_header == NULL)
-    return -1;
-
-  struct curl_slist *headers = NULL;
-  headers = curl_slist_append(headers, authorization_header);
-  headers = curl_slist_append(headers, "Content-Type: application/json");
-
-  curl_easy_setopt(cb->curl, CURLOPT_URL, final_url);
-  curl_easy_setopt(cb->curl, CURLOPT_HTTPHEADER, headers);
-  curl_easy_setopt(cb->curl, CURLOPT_POST, 1L);
-  curl_easy_setopt(cb->curl, CURLOPT_POSTFIELDS, payload);
-
-  wg_memory_t res = {
-      .memory = NULL, .size = 0,
-  };
-  curl_easy_setopt(cb->curl, CURLOPT_WRITEFUNCTION, wg_write_memory_cb);
-  curl_easy_setopt(cb->curl, CURLOPT_WRITEDATA, &res);
-
-  status = curl_easy_perform(cb->curl);
-  if (status != CURLE_OK) {
-    ERROR("write_gcm plugin: curl_easy_perform failed with status %d: %s",
-          status, cb->curl_errbuf);
-    sfree(res.memory);
-    curl_slist_free_all(headers);
-    sfree(authorization_header);
-    return -1;
-  }
-
-  long http_code = 0;
-  curl_easy_getinfo(cb->curl, CURLINFO_RESPONSE_CODE, &http_code);
-  if ((http_code < 200) || (http_code >= 300)) {
-    ERROR("write_gcm plugin: POST request to %s failed: HTTP error %ld",
-          final_url, http_code);
-    INFO("write_gcm plugin: Server replied: %s", res.memory);
-    sfree(res.memory);
-    curl_slist_free_all(headers);
-    sfree(authorization_header);
-    return -1;
-  }
-
-  sfree(res.memory);
-  curl_slist_free_all(headers);
-  sfree(authorization_header);
-  return status;
-} /* }}} wg_call_timeseries_write */
-
-static int wg_callback_init(wg_callback_t *cb) /* {{{ */
-{
-  if (cb->curl != NULL)
-    return 0;
-
-  cb->formatter = gcm_output_create(cb->resource);
-  if (cb->formatter == NULL) {
-    ERROR("write_gcm plugin: gcm_output_create failed.");
-    return -1;
-  }
-
-  cb->curl = curl_easy_init();
-  if (cb->curl == NULL) {
-    ERROR("write_gcm plugin: curl_easy_init failed.");
-    return -1;
-  }
-
-  curl_easy_setopt(cb->curl, CURLOPT_NOSIGNAL, 1L);
-  curl_easy_setopt(cb->curl, CURLOPT_USERAGENT,
-                   PACKAGE_NAME "/" PACKAGE_VERSION);
-  curl_easy_setopt(cb->curl, CURLOPT_ERRORBUFFER, cb->curl_errbuf);
-  wg_reset_buffer(cb);
-
-  return 0;
-} /* }}} int wg_callback_init */
-
-static int wg_flush_nolock(cdtime_t timeout, wg_callback_t *cb) /* {{{ */
-{
-  if (cb->timeseries_count == 0) {
-    cb->send_buffer_init_time = cdtime();
-    return 0;
-  }
-
-  /* timeout == 0  => flush unconditionally */
-  if (timeout > 0) {
-    cdtime_t now = cdtime();
-
-    if ((cb->send_buffer_init_time + timeout) > now)
-      return 0;
-  }
-
-  char *payload = gcm_output_reset(cb->formatter);
-  int status = wg_call_timeseries_write(cb, payload);
-  if (status != 0) {
-    ERROR("write_gcm plugin: Sending buffer failed with status %d.", status);
-  }
-
-  wg_reset_buffer(cb);
-  return status;
-} /* }}} wg_flush_nolock */
-
-static int wg_flush(cdtime_t timeout, /* {{{ */
-                    const char *identifier __attribute__((unused)),
-                    user_data_t *user_data) {
-  wg_callback_t *cb;
-  int status;
-
-  if (user_data == NULL)
-    return -EINVAL;
-
-  cb = user_data->data;
-
-  pthread_mutex_lock(&cb->lock);
-
-  if (cb->curl == NULL) {
-    status = wg_callback_init(cb);
-    if (status != 0) {
-      ERROR("write_gcm plugin: wg_callback_init failed.");
-      pthread_mutex_unlock(&cb->lock);
-      return -1;
-    }
-  }
-
-  status = wg_flush_nolock(timeout, cb);
-  pthread_mutex_unlock(&cb->lock);
-
-  return status;
-} /* }}} int wg_flush */
-
-static void wg_callback_free(void *data) /* {{{ */
-{
-  wg_callback_t *cb = data;
-  if (cb == NULL)
-    return;
-
-  gcm_output_destroy(cb->formatter);
-  cb->formatter = NULL;
-
-  sfree(cb->email);
-  sfree(cb->project);
-  sfree(cb->url);
-
-  oauth_destroy(cb->auth);
-  if (cb->curl) {
-    curl_easy_cleanup(cb->curl);
-  }
-
-  sfree(cb);
-} /* }}} void wg_callback_free */
-
-static int wg_metric_descriptors_create(wg_callback_t *cb, const data_set_t *ds,
-                                        const value_list_t *vl) {
-  /* {{{ */
-  for (size_t i = 0; i < ds->ds_num; i++) {
-    char buffer[4096];
-
-    int status =
-        gcm_format_metric_descriptor(buffer, sizeof(buffer), ds, vl, i);
-    if (status != 0) {
-      ERROR("write_gcm plugin: gcm_format_metric_descriptor failed with status "
-            "%d",
-            status);
-      return status;
-    }
-
-    status = wg_call_metricdescriptor_create(cb, buffer);
-    if (status != 0) {
-      ERROR("write_gcm plugin: wg_call_metricdescriptor_create failed with "
-            "status %d",
-            status);
-      return status;
-    }
-  }
-
-  return gcm_output_register_metric(cb->formatter, ds, vl);
-} /* }}} int wg_metric_descriptors_create */
-
-static int wg_write(const data_set_t *ds, const value_list_t *vl, /* {{{ */
-                    user_data_t *user_data) {
-  wg_callback_t *cb = user_data->data;
-  if (cb == NULL)
-    return EINVAL;
-
-  pthread_mutex_lock(&cb->lock);
-
-  if (cb->curl == NULL) {
-    int status = wg_callback_init(cb);
-    if (status != 0) {
-      ERROR("write_gcm plugin: wg_callback_init failed.");
-      pthread_mutex_unlock(&cb->lock);
-      return status;
-    }
-  }
-
-  int status;
-  while (42) {
-    status = gcm_output_add(cb->formatter, ds, vl);
-    if (status == 0) { /* success */
-      break;
-    } else if (status == ENOBUFS) { /* success, flush */
-      wg_flush_nolock(0, cb);
-      status = 0;
-      break;
-    } else if (status == EEXIST) {
-      /* metric already in the buffer; flush and retry */
-      wg_flush_nolock(0, cb);
-      continue;
-    } else if (status == ENOENT) {
-      /* new metric, create metric descriptor first */
-      status = wg_metric_descriptors_create(cb, ds, vl);
-      if (status != 0) {
-        break;
-      }
-      continue;
-    } else {
-      break;
-    }
-  }
-
-  if (status == 0) {
-    cb->timeseries_count++;
-  }
-
-  pthread_mutex_unlock(&cb->lock);
-  return status;
-} /* }}} int wg_write */
-
-static void wg_check_scope(char const *email) /* {{{ */
-{
-  char *scope = gce_scope(email);
-  if (scope == NULL) {
-    WARNING("write_gcm plugin: Unable to determine scope of this instance.");
-    return;
-  }
-
-  if (strstr(scope, MONITORING_SCOPE) == NULL) {
-    size_t scope_len;
-
-    /* Strip trailing newline characers for printing. */
-    scope_len = strlen(scope);
-    while ((scope_len > 0) && (iscntrl((int)scope[scope_len - 1])))
-      scope[--scope_len] = 0;
-
-    WARNING("write_gcm plugin: The determined scope of this instance "
-            "(\"%s\") does not contain the monitoring scope (\"%s\"). You need "
-            "to add this scope to the list of scopes passed to gcutil with "
-            "--service_account_scopes when creating the instance. "
-            "Alternatively, to use this plugin on an instance which does not "
-            "have this scope, use a Service Account.",
-            scope, MONITORING_SCOPE);
-  }
-
-  sfree(scope);
-} /* }}} void wg_check_scope */
-
-static int wg_config_resource(oconfig_item_t *ci, wg_callback_t *cb) /* {{{ */
-{
-  if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_STRING)) {
-    ERROR("write_gcm plugin: The \"%s\" option requires exactly one string "
-          "argument.",
-          ci->key);
-    return EINVAL;
-  }
-  char *resource_type = ci->values[0].value.string;
-
-  if (cb->resource != NULL) {
-    gcm_resource_destroy(cb->resource);
-  }
-
-  cb->resource = gcm_resource_create(resource_type);
-  if (cb->resource == NULL) {
-    ERROR("write_gcm plugin: gcm_resource_create(\"%s\") failed.",
-          resource_type);
-    return ENOMEM;
-  }
-
-  for (int i = 0; i < ci->children_num; i++) {
-    oconfig_item_t *child = ci->children + i;
-
-    if ((child->values_num != 1) ||
-        (child->values[0].type != OCONFIG_TYPE_STRING)) {
-      ERROR("write_gcm plugin: Resource labels must have exactly one string "
-            "value. Ignoring label \"%s\".",
-            child->key);
-      continue;
-    }
-
-    gcm_resource_add_label(cb->resource, child->key,
-                           child->values[0].value.string);
-  }
-
-  return 0;
-} /* }}} int wg_config_resource */
-
-static int wg_config(oconfig_item_t *ci) /* {{{ */
-{
-  if (ci == NULL) {
-    return EINVAL;
-  }
-
-  wg_callback_t *cb = calloc(1, sizeof(*cb));
-  if (cb == NULL) {
-    ERROR("write_gcm plugin: calloc failed.");
-    return ENOMEM;
-  }
-  cb->url = strdup(GCM_API_URL);
-  pthread_mutex_init(&cb->lock, /* attr = */ NULL);
-
-  char *credential_file = NULL;
-
-  for (int i = 0; i < ci->children_num; i++) {
-    oconfig_item_t *child = ci->children + i;
-    if (strcasecmp("Project", child->key) == 0)
-      cf_util_get_string(child, &cb->project);
-    else if (strcasecmp("Email", child->key) == 0)
-      cf_util_get_string(child, &cb->email);
-    else if (strcasecmp("Url", child->key) == 0)
-      cf_util_get_string(child, &cb->url);
-    else if (strcasecmp("CredentialFile", child->key) == 0)
-      cf_util_get_string(child, &credential_file);
-    else if (strcasecmp("Resource", child->key) == 0)
-      wg_config_resource(child, cb);
-    else {
-      ERROR("write_gcm plugin: Invalid configuration option: %s.", child->key);
-      wg_callback_free(cb);
-      return EINVAL;
-    }
-  }
-
-  /* Set up authentication */
-  /* Option 1: Credentials file given => use service account */
-  if (credential_file != NULL) {
-    oauth_google_t cfg =
-        oauth_create_google_file(credential_file, MONITORING_SCOPE);
-    if (cfg.oauth == NULL) {
-      ERROR("write_gcm plugin: oauth_create_google_file failed");
-      wg_callback_free(cb);
-      return EINVAL;
-    }
-    cb->auth = cfg.oauth;
-
-    if (cb->project == NULL) {
-      cb->project = cfg.project_id;
-      INFO("write_gcm plugin: Automatically detected project ID: \"%s\"",
-           cb->project);
-    } else {
-      sfree(cfg.project_id);
-    }
-  }
-  /* Option 2: Look for credentials in well-known places */
-  if (cb->auth == NULL) {
-    oauth_google_t cfg = oauth_create_google_default(MONITORING_SCOPE);
-    cb->auth = cfg.oauth;
-
-    if (cb->project == NULL) {
-      cb->project = cfg.project_id;
-      INFO("write_gcm plugin: Automatically detected project ID: \"%s\"",
-           cb->project);
-    } else {
-      sfree(cfg.project_id);
-    }
-  }
-
-  if ((cb->auth != NULL) && (cb->email != NULL)) {
-    NOTICE("write_gcm plugin: A service account email was configured but is "
-           "not used for authentication because %s used instead.",
-           (credential_file != NULL) ? "a credential file was"
-                                     : "application default credentials were");
-  }
-
-  /* Option 3: Running on GCE => use metadata service */
-  if ((cb->auth == NULL) && gce_check()) {
-    wg_check_scope(cb->email);
-  } else if (cb->auth == NULL) {
-    ERROR("write_gcm plugin: Unable to determine credentials. Please either "
-          "specify the \"Credentials\" option or set up Application Default "
-          "Credentials.");
-    wg_callback_free(cb);
-    return EINVAL;
-  }
-
-  if ((cb->project == NULL) && gce_check()) {
-    cb->project = gce_project_id();
-  }
-  if (cb->project == NULL) {
-    ERROR("write_gcm plugin: Unable to determine the project number. "
-          "Please specify the \"Project\" option manually.");
-    wg_callback_free(cb);
-    return EINVAL;
-  }
-
-  if ((cb->resource == NULL) && gce_check()) {
-    /* TODO(octo): add error handling */
-    cb->resource = gcm_resource_create("gce_instance");
-    gcm_resource_add_label(cb->resource, "project_id", gce_project_id());
-    gcm_resource_add_label(cb->resource, "instance_id", gce_instance_id());
-    gcm_resource_add_label(cb->resource, "zone", gce_zone());
-  }
-  if (cb->resource == NULL) {
-    /* TODO(octo): add error handling */
-    cb->resource = gcm_resource_create("global");
-    gcm_resource_add_label(cb->resource, "project_id", cb->project);
-  }
-
-  DEBUG("write_gcm plugin: Registering write callback with URL %s", cb->url);
-  assert((cb->auth != NULL) || gce_check());
-
-  user_data_t user_data = {
-      .data = cb,
-  };
-  plugin_register_flush("write_gcm", wg_flush, &user_data);
-
-  user_data.free_func = wg_callback_free;
-  plugin_register_write("write_gcm", wg_write, &user_data);
-
-  return 0;
-} /* }}} int wg_config */
-
-static int wg_init(void) {
-  /* {{{ */
-  /* Call this while collectd is still single-threaded to avoid
-   * initialization issues in libgcrypt. */
-  curl_global_init(CURL_GLOBAL_SSL);
-
-  return 0;
-} /* }}} int wg_init */
-
-void module_register(void) /* {{{ */
-{
-  plugin_register_complex_config("write_gcm", wg_config);
-  plugin_register_init("write_gcm", wg_init);
-} /* }}} void module_register */
-
-/* vim: set sw=2 sts=2 et fdm=marker : */
diff --git a/src/write_stackdriver.c b/src/write_stackdriver.c
new file mode 100644 (file)
index 0000000..e6d3b74
--- /dev/null
@@ -0,0 +1,650 @@
+/**
+ * collectd - src/write_stackdriver.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 "common.h"
+#include "configfile.h"
+#include "plugin.h"
+#include "utils_format_stackdriver.h"
+#include "utils_gce.h"
+#include "utils_oauth.h"
+
+#include <curl/curl.h>
+#include <pthread.h>
+
+/*
+ * Private variables
+ */
+#ifndef GCM_API_URL
+#define GCM_API_URL "https://monitoring.googleapis.com/v3"
+#endif
+
+#ifndef MONITORING_SCOPE
+#define MONITORING_SCOPE "https://www.googleapis.com/auth/monitoring"
+#endif
+
+struct wg_callback_s {
+  /* config */
+  char *email;
+  char *project;
+  char *url;
+  sd_resource_t *resource;
+
+  /* runtime */
+  oauth_t *auth;
+  sd_output_t *formatter;
+  CURL *curl;
+  char curl_errbuf[CURL_ERROR_SIZE];
+  /* used by flush */
+  size_t timeseries_count;
+  cdtime_t send_buffer_init_time;
+
+  pthread_mutex_t lock;
+};
+typedef struct wg_callback_s wg_callback_t;
+
+struct wg_memory_s {
+  char *memory;
+  size_t size;
+};
+typedef struct wg_memory_s wg_memory_t;
+
+static size_t wg_write_memory_cb(void *contents, size_t size,
+                                 size_t nmemb, /* {{{ */
+                                 void *userp) {
+  size_t realsize = size * nmemb;
+  wg_memory_t *mem = (wg_memory_t *)userp;
+
+  if (0x7FFFFFF0 < mem->size || 0x7FFFFFF0 - mem->size < realsize) {
+    ERROR("integer overflow");
+    return 0;
+  }
+
+  mem->memory = (char *)realloc((void *)mem->memory, mem->size + realsize + 1);
+  if (mem->memory == NULL) {
+    /* out of memory! */
+    ERROR("wg_write_memory_cb: not enough memory (realloc returned NULL)");
+    return 0;
+  }
+
+  memcpy(&(mem->memory[mem->size]), contents, realsize);
+  mem->size += realsize;
+  mem->memory[mem->size] = 0;
+  return realsize;
+} /* }}} size_t wg_write_memory_cb */
+
+static char *wg_get_authorization_header(wg_callback_t *cb) { /* {{{ */
+  int status = 0;
+  char access_token[256];
+  char authorization_header[256];
+
+  assert((cb->auth != NULL) || gce_check());
+  if (cb->auth != NULL)
+    status = oauth_access_token(cb->auth, access_token, sizeof(access_token));
+  else
+    status = gce_access_token(cb->email, access_token, sizeof(access_token));
+  if (status != 0) {
+    ERROR("write_stackdriver plugin: Failed to get access token");
+    return NULL;
+  }
+
+  status = snprintf(authorization_header, sizeof(authorization_header),
+                    "Authorization: Bearer %s", access_token);
+  if ((status < 1) || ((size_t)status >= sizeof(authorization_header)))
+    return NULL;
+
+  return strdup(authorization_header);
+} /* }}} char *wg_get_authorization_header */
+
+static int wg_call_metricdescriptor_create(wg_callback_t *cb,
+                                           char const *payload) {
+  /* {{{ */
+  char final_url[1024];
+  int status =
+      snprintf(final_url, sizeof(final_url), "%s/projects/%s/metricDescriptors",
+               cb->url, cb->project);
+  if ((status < 1) || ((size_t)status >= sizeof(final_url)))
+    return -1;
+
+  char *authorization_header = wg_get_authorization_header(cb);
+  if (authorization_header == NULL)
+    return -1;
+
+  struct curl_slist *headers = NULL;
+  headers = curl_slist_append(headers, "Content-Type: application/json");
+  headers = curl_slist_append(headers, authorization_header);
+
+  CURL *curl = curl_easy_init();
+  if (!curl) {
+    ERROR("write_stackdriver plugin: curl_easy_init failed.");
+    curl_slist_free_all(headers);
+    sfree(authorization_header);
+    return -1;
+  }
+
+  curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
+  curl_easy_setopt(cb->curl, CURLOPT_USERAGENT,
+                   PACKAGE_NAME "/" PACKAGE_VERSION);
+  char curl_errbuf[CURL_ERROR_SIZE];
+  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
+  curl_easy_setopt(curl, CURLOPT_URL, final_url);
+  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+  curl_easy_setopt(curl, CURLOPT_POST, 1L);
+  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
+
+  wg_memory_t res = {
+      .memory = NULL, .size = 0,
+  };
+  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, wg_write_memory_cb);
+  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res);
+
+  status = curl_easy_perform(curl);
+  if (status != CURLE_OK) {
+    ERROR(
+        "write_stackdriver plugin: curl_easy_perform failed with status %d: %s",
+        status, curl_errbuf);
+    sfree(res.memory);
+    curl_easy_cleanup(curl);
+    curl_slist_free_all(headers);
+    sfree(authorization_header);
+    return -1;
+  }
+
+  long http_code = 0;
+  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
+  if ((http_code < 200) || (http_code >= 300)) {
+    ERROR("write_stackdriver plugin: POST request to %s failed: HTTP error %ld",
+          final_url, http_code);
+    INFO("write_stackdriver plugin: Server replied: %s", res.memory);
+    sfree(res.memory);
+    curl_easy_cleanup(curl);
+    curl_slist_free_all(headers);
+    sfree(authorization_header);
+    return -1;
+  }
+
+  sfree(res.memory);
+  curl_easy_cleanup(curl);
+  curl_slist_free_all(headers);
+  sfree(authorization_header);
+  return 0;
+} /* }}} int wg_call_metricdescriptor_create */
+
+static void wg_reset_buffer(wg_callback_t *cb) /* {{{ */
+{
+  cb->timeseries_count = 0;
+  cb->send_buffer_init_time = cdtime();
+} /* }}} wg_reset_buffer */
+
+static int wg_call_timeseries_write(wg_callback_t *cb,
+                                    char const *payload) /* {{{ */
+{
+  char final_url[1024];
+  int status = snprintf(final_url, sizeof(final_url),
+                        "%s/projects/%s/timeSeries", cb->url, cb->project);
+  if ((status < 1) || ((size_t)status >= sizeof(final_url)))
+    return -1;
+
+  char *authorization_header = wg_get_authorization_header(cb);
+  if (authorization_header == NULL)
+    return -1;
+
+  struct curl_slist *headers = NULL;
+  headers = curl_slist_append(headers, authorization_header);
+  headers = curl_slist_append(headers, "Content-Type: application/json");
+
+  curl_easy_setopt(cb->curl, CURLOPT_URL, final_url);
+  curl_easy_setopt(cb->curl, CURLOPT_HTTPHEADER, headers);
+  curl_easy_setopt(cb->curl, CURLOPT_POST, 1L);
+  curl_easy_setopt(cb->curl, CURLOPT_POSTFIELDS, payload);
+
+  wg_memory_t res = {
+      .memory = NULL, .size = 0,
+  };
+  curl_easy_setopt(cb->curl, CURLOPT_WRITEFUNCTION, wg_write_memory_cb);
+  curl_easy_setopt(cb->curl, CURLOPT_WRITEDATA, &res);
+
+  status = curl_easy_perform(cb->curl);
+  if (status != CURLE_OK) {
+    ERROR(
+        "write_stackdriver plugin: curl_easy_perform failed with status %d: %s",
+        status, cb->curl_errbuf);
+    sfree(res.memory);
+    curl_slist_free_all(headers);
+    sfree(authorization_header);
+    return -1;
+  }
+
+  long http_code = 0;
+  curl_easy_getinfo(cb->curl, CURLINFO_RESPONSE_CODE, &http_code);
+  if ((http_code < 200) || (http_code >= 300)) {
+    ERROR("write_stackdriver plugin: POST request to %s failed: HTTP error %ld",
+          final_url, http_code);
+    INFO("write_stackdriver plugin: Server replied: %s", res.memory);
+    sfree(res.memory);
+    curl_slist_free_all(headers);
+    sfree(authorization_header);
+    return -1;
+  }
+
+  sfree(res.memory);
+  curl_slist_free_all(headers);
+  sfree(authorization_header);
+  return status;
+} /* }}} wg_call_timeseries_write */
+
+static int wg_callback_init(wg_callback_t *cb) /* {{{ */
+{
+  if (cb->curl != NULL)
+    return 0;
+
+  cb->formatter = sd_output_create(cb->resource);
+  if (cb->formatter == NULL) {
+    ERROR("write_stackdriver plugin: sd_output_create failed.");
+    return -1;
+  }
+
+  cb->curl = curl_easy_init();
+  if (cb->curl == NULL) {
+    ERROR("write_stackdriver plugin: curl_easy_init failed.");
+    return -1;
+  }
+
+  curl_easy_setopt(cb->curl, CURLOPT_NOSIGNAL, 1L);
+  curl_easy_setopt(cb->curl, CURLOPT_USERAGENT,
+                   PACKAGE_NAME "/" PACKAGE_VERSION);
+  curl_easy_setopt(cb->curl, CURLOPT_ERRORBUFFER, cb->curl_errbuf);
+  wg_reset_buffer(cb);
+
+  return 0;
+} /* }}} int wg_callback_init */
+
+static int wg_flush_nolock(cdtime_t timeout, wg_callback_t *cb) /* {{{ */
+{
+  if (cb->timeseries_count == 0) {
+    cb->send_buffer_init_time = cdtime();
+    return 0;
+  }
+
+  /* timeout == 0  => flush unconditionally */
+  if (timeout > 0) {
+    cdtime_t now = cdtime();
+
+    if ((cb->send_buffer_init_time + timeout) > now)
+      return 0;
+  }
+
+  char *payload = sd_output_reset(cb->formatter);
+  int status = wg_call_timeseries_write(cb, payload);
+  if (status != 0) {
+    ERROR("write_stackdriver plugin: Sending buffer failed with status %d.",
+          status);
+  }
+
+  wg_reset_buffer(cb);
+  return status;
+} /* }}} wg_flush_nolock */
+
+static int wg_flush(cdtime_t timeout, /* {{{ */
+                    const char *identifier __attribute__((unused)),
+                    user_data_t *user_data) {
+  wg_callback_t *cb;
+  int status;
+
+  if (user_data == NULL)
+    return -EINVAL;
+
+  cb = user_data->data;
+
+  pthread_mutex_lock(&cb->lock);
+
+  if (cb->curl == NULL) {
+    status = wg_callback_init(cb);
+    if (status != 0) {
+      ERROR("write_stackdriver plugin: wg_callback_init failed.");
+      pthread_mutex_unlock(&cb->lock);
+      return -1;
+    }
+  }
+
+  status = wg_flush_nolock(timeout, cb);
+  pthread_mutex_unlock(&cb->lock);
+
+  return status;
+} /* }}} int wg_flush */
+
+static void wg_callback_free(void *data) /* {{{ */
+{
+  wg_callback_t *cb = data;
+  if (cb == NULL)
+    return;
+
+  sd_output_destroy(cb->formatter);
+  cb->formatter = NULL;
+
+  sfree(cb->email);
+  sfree(cb->project);
+  sfree(cb->url);
+
+  oauth_destroy(cb->auth);
+  if (cb->curl) {
+    curl_easy_cleanup(cb->curl);
+  }
+
+  sfree(cb);
+} /* }}} void wg_callback_free */
+
+static int wg_metric_descriptors_create(wg_callback_t *cb, const data_set_t *ds,
+                                        const value_list_t *vl) {
+  /* {{{ */
+  for (size_t i = 0; i < ds->ds_num; i++) {
+    char buffer[4096];
+
+    int status = sd_format_metric_descriptor(buffer, sizeof(buffer), ds, vl, i);
+    if (status != 0) {
+      ERROR("write_stackdriver plugin: sd_format_metric_descriptor failed "
+            "with status "
+            "%d",
+            status);
+      return status;
+    }
+
+    status = wg_call_metricdescriptor_create(cb, buffer);
+    if (status != 0) {
+      ERROR("write_stackdriver plugin: wg_call_metricdescriptor_create failed "
+            "with "
+            "status %d",
+            status);
+      return status;
+    }
+  }
+
+  return sd_output_register_metric(cb->formatter, ds, vl);
+} /* }}} int wg_metric_descriptors_create */
+
+static int wg_write(const data_set_t *ds, const value_list_t *vl, /* {{{ */
+                    user_data_t *user_data) {
+  wg_callback_t *cb = user_data->data;
+  if (cb == NULL)
+    return EINVAL;
+
+  pthread_mutex_lock(&cb->lock);
+
+  if (cb->curl == NULL) {
+    int status = wg_callback_init(cb);
+    if (status != 0) {
+      ERROR("write_stackdriver plugin: wg_callback_init failed.");
+      pthread_mutex_unlock(&cb->lock);
+      return status;
+    }
+  }
+
+  int status;
+  while (42) {
+    status = sd_output_add(cb->formatter, ds, vl);
+    if (status == 0) { /* success */
+      break;
+    } else if (status == ENOBUFS) { /* success, flush */
+      wg_flush_nolock(0, cb);
+      status = 0;
+      break;
+    } else if (status == EEXIST) {
+      /* metric already in the buffer; flush and retry */
+      wg_flush_nolock(0, cb);
+      continue;
+    } else if (status == ENOENT) {
+      /* new metric, create metric descriptor first */
+      status = wg_metric_descriptors_create(cb, ds, vl);
+      if (status != 0) {
+        break;
+      }
+      continue;
+    } else {
+      break;
+    }
+  }
+
+  if (status == 0) {
+    cb->timeseries_count++;
+  }
+
+  pthread_mutex_unlock(&cb->lock);
+  return status;
+} /* }}} int wg_write */
+
+static void wg_check_scope(char const *email) /* {{{ */
+{
+  char *scope = gce_scope(email);
+  if (scope == NULL) {
+    WARNING("write_stackdriver plugin: Unable to determine scope of this "
+            "instance.");
+    return;
+  }
+
+  if (strstr(scope, MONITORING_SCOPE) == NULL) {
+    size_t scope_len;
+
+    /* Strip trailing newline characers for printing. */
+    scope_len = strlen(scope);
+    while ((scope_len > 0) && (iscntrl((int)scope[scope_len - 1])))
+      scope[--scope_len] = 0;
+
+    WARNING("write_stackdriver plugin: The determined scope of this instance "
+            "(\"%s\") does not contain the monitoring scope (\"%s\"). You need "
+            "to add this scope to the list of scopes passed to gcutil with "
+            "--service_account_scopes when creating the instance. "
+            "Alternatively, to use this plugin on an instance which does not "
+            "have this scope, use a Service Account.",
+            scope, MONITORING_SCOPE);
+  }
+
+  sfree(scope);
+} /* }}} void wg_check_scope */
+
+static int wg_config_resource(oconfig_item_t *ci, wg_callback_t *cb) /* {{{ */
+{
+  if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_STRING)) {
+    ERROR("write_stackdriver plugin: The \"%s\" option requires exactly one "
+          "string "
+          "argument.",
+          ci->key);
+    return EINVAL;
+  }
+  char *resource_type = ci->values[0].value.string;
+
+  if (cb->resource != NULL) {
+    sd_resource_destroy(cb->resource);
+  }
+
+  cb->resource = sd_resource_create(resource_type);
+  if (cb->resource == NULL) {
+    ERROR("write_stackdriver plugin: sd_resource_create(\"%s\") failed.",
+          resource_type);
+    return ENOMEM;
+  }
+
+  for (int i = 0; i < ci->children_num; i++) {
+    oconfig_item_t *child = ci->children + i;
+
+    if ((child->values_num != 1) ||
+        (child->values[0].type != OCONFIG_TYPE_STRING)) {
+      ERROR("write_stackdriver plugin: Resource labels must have exactly one "
+            "string "
+            "value. Ignoring label \"%s\".",
+            child->key);
+      continue;
+    }
+
+    sd_resource_add_label(cb->resource, child->key,
+                          child->values[0].value.string);
+  }
+
+  return 0;
+} /* }}} int wg_config_resource */
+
+static int wg_config(oconfig_item_t *ci) /* {{{ */
+{
+  if (ci == NULL) {
+    return EINVAL;
+  }
+
+  wg_callback_t *cb = calloc(1, sizeof(*cb));
+  if (cb == NULL) {
+    ERROR("write_stackdriver plugin: calloc failed.");
+    return ENOMEM;
+  }
+  cb->url = strdup(GCM_API_URL);
+  pthread_mutex_init(&cb->lock, /* attr = */ NULL);
+
+  char *credential_file = NULL;
+
+  for (int i = 0; i < ci->children_num; i++) {
+    oconfig_item_t *child = ci->children + i;
+    if (strcasecmp("Project", child->key) == 0)
+      cf_util_get_string(child, &cb->project);
+    else if (strcasecmp("Email", child->key) == 0)
+      cf_util_get_string(child, &cb->email);
+    else if (strcasecmp("Url", child->key) == 0)
+      cf_util_get_string(child, &cb->url);
+    else if (strcasecmp("CredentialFile", child->key) == 0)
+      cf_util_get_string(child, &credential_file);
+    else if (strcasecmp("Resource", child->key) == 0)
+      wg_config_resource(child, cb);
+    else {
+      ERROR("write_stackdriver plugin: Invalid configuration option: %s.",
+            child->key);
+      wg_callback_free(cb);
+      return EINVAL;
+    }
+  }
+
+  /* Set up authentication */
+  /* Option 1: Credentials file given => use service account */
+  if (credential_file != NULL) {
+    oauth_google_t cfg =
+        oauth_create_google_file(credential_file, MONITORING_SCOPE);
+    if (cfg.oauth == NULL) {
+      ERROR("write_stackdriver plugin: oauth_create_google_file failed");
+      wg_callback_free(cb);
+      return EINVAL;
+    }
+    cb->auth = cfg.oauth;
+
+    if (cb->project == NULL) {
+      cb->project = cfg.project_id;
+      INFO(
+          "write_stackdriver plugin: Automatically detected project ID: \"%s\"",
+          cb->project);
+    } else {
+      sfree(cfg.project_id);
+    }
+  }
+  /* Option 2: Look for credentials in well-known places */
+  if (cb->auth == NULL) {
+    oauth_google_t cfg = oauth_create_google_default(MONITORING_SCOPE);
+    cb->auth = cfg.oauth;
+
+    if (cb->project == NULL) {
+      cb->project = cfg.project_id;
+      INFO(
+          "write_stackdriver plugin: Automatically detected project ID: \"%s\"",
+          cb->project);
+    } else {
+      sfree(cfg.project_id);
+    }
+  }
+
+  if ((cb->auth != NULL) && (cb->email != NULL)) {
+    NOTICE("write_stackdriver plugin: A service account email was configured "
+           "but is "
+           "not used for authentication because %s used instead.",
+           (credential_file != NULL) ? "a credential file was"
+                                     : "application default credentials were");
+  }
+
+  /* Option 3: Running on GCE => use metadata service */
+  if ((cb->auth == NULL) && gce_check()) {
+    wg_check_scope(cb->email);
+  } else if (cb->auth == NULL) {
+    ERROR("write_stackdriver plugin: Unable to determine credentials. Please "
+          "either "
+          "specify the \"Credentials\" option or set up Application Default "
+          "Credentials.");
+    wg_callback_free(cb);
+    return EINVAL;
+  }
+
+  if ((cb->project == NULL) && gce_check()) {
+    cb->project = gce_project_id();
+  }
+  if (cb->project == NULL) {
+    ERROR("write_stackdriver plugin: Unable to determine the project number. "
+          "Please specify the \"Project\" option manually.");
+    wg_callback_free(cb);
+    return EINVAL;
+  }
+
+  if ((cb->resource == NULL) && gce_check()) {
+    /* TODO(octo): add error handling */
+    cb->resource = sd_resource_create("gce_instance");
+    sd_resource_add_label(cb->resource, "project_id", gce_project_id());
+    sd_resource_add_label(cb->resource, "instance_id", gce_instance_id());
+    sd_resource_add_label(cb->resource, "zone", gce_zone());
+  }
+  if (cb->resource == NULL) {
+    /* TODO(octo): add error handling */
+    cb->resource = sd_resource_create("global");
+    sd_resource_add_label(cb->resource, "project_id", cb->project);
+  }
+
+  DEBUG("write_stackdriver plugin: Registering write callback with URL %s",
+        cb->url);
+  assert((cb->auth != NULL) || gce_check());
+
+  user_data_t user_data = {
+      .data = cb,
+  };
+  plugin_register_flush("write_stackdriver", wg_flush, &user_data);
+
+  user_data.free_func = wg_callback_free;
+  plugin_register_write("write_stackdriver", wg_write, &user_data);
+
+  return 0;
+} /* }}} int wg_config */
+
+static int wg_init(void) {
+  /* {{{ */
+  /* Call this while collectd is still single-threaded to avoid
+   * initialization issues in libgcrypt. */
+  curl_global_init(CURL_GLOBAL_SSL);
+
+  return 0;
+} /* }}} int wg_init */
+
+void module_register(void) /* {{{ */
+{
+  plugin_register_complex_config("write_stackdriver", wg_config);
+  plugin_register_init("write_stackdriver", wg_init);
+} /* }}} void module_register */
+
+/* vim: set sw=2 sts=2 et fdm=marker : */