From: Florian Forster Date: Mon, 6 Nov 2017 20:26:08 +0000 (+0100) Subject: Rename write_gcm to write_stackdriver. X-Git-Url: https://git.verplant.org/?a=commitdiff_plain;h=2f7fd156e92952dc478d3735e79af7d344a1eba1;p=collectd.git Rename write_gcm to write_stackdriver. --- diff --git a/Makefile.am b/Makefile.am index 93a4ca89..266ff10c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 diff --git a/configure.ac b/configure.ac index 92c81eeb..5333c423 100644 --- a/configure.ac +++ b/configure.ac @@ -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]) diff --git a/src/collectd.conf.in b/src/collectd.conf.in index 6a096430..184d0b93 100644 --- a/src/collectd.conf.in +++ b/src/collectd.conf.in @@ -213,7 +213,6 @@ #@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 @@ -223,6 +222,7 @@ #@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 @@ -1675,16 +1675,6 @@ # Verbose false # -# -# Project "stackdriver-account" -# CredentialFile "/path/to/gcp-project-id-12345.json" -# Email "123456789012@developer.gserviceaccount.com" -# -# project_id "gcp-project-id" -# -# Url "https://monitoring.googleapis.com/v3" -# - # # # Host "localhost" @@ -1798,6 +1788,16 @@ # Attribute "foo" "bar" # +# +# Project "stackdriver-account" +# CredentialFile "/path/to/gcp-project-id-12345.json" +# Email "123456789012@developer.gserviceaccount.com" +# +# project_id "gcp-project-id" +# +# Url "https://monitoring.googleapis.com/v3" +# + # # # Host "localhost" diff --git a/src/collectd.conf.pod b/src/collectd.conf.pod index e293d495..7ff36b2c 100644 --- a/src/collectd.conf.pod +++ b/src/collectd.conf.pod @@ -9337,86 +9337,6 @@ traffic (e.Eg. 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 - -The C plugin writes metrics to the I (GCM) -service. - -This plugin supports two authentication methods: When configured, credentials -are read from the JSON credentials file specified with B. -Alternatively, when running on -I (GCE), an I token is retrieved from the -I and used to authenticate to GCM. - -B - - - CredentialFile "/path/to/service_account.json" - - project_id "monitored_project" - - - -=over 4 - -=item B I - -Path to a JSON credentials file holding the credentials for a GCP service -account. - -If not specified, I. If running on GCE, -B may be set to chose a different service account associated with the -instance. - -=item B I - -The I or the I of the I. The -I is a string identifying the GCP project, which you can chose -freely when creating a new project. The I is a 12-digit decimal -number. You can look up both on the I. - -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 I - -Email address of an GCE I. This setting is only effective when -running on GCE and using I (see -B above). - -=item B I - -Configures the I to use when storing metrics. This option -takes a I and arbitrary string options which are used as labels. - -On GCE, defaults to the equivalent of this config: - - - project_id "${meta/project/project-id}" - instance_id "${meta/instance/id}" - zone "${meta/instance/zone}" - - -Where C<${meta/...}> are values read from the meta data service. - -When not running on GCE, defaults to the equivalent of this config: - - - project_id "${Project}" - - -Where C<${Project}> refers to the B option. - -See L for more information -on I. - -=item B I - -URL of the I API. Defaults to -C. - -=back - =head2 Plugin C The C plugin writes data to I, an open-source metrics @@ -10387,6 +10307,86 @@ attribute for each metric being sent out to I. =back +=head2 Plugin C + +The C plugin writes metrics to the +I service. + +This plugin supports two authentication methods: When configured, credentials +are read from the JSON credentials file specified with B. +Alternatively, when running on +I (GCE), an I token is retrieved from the +I and used to authenticate to GCM. + +B + + + CredentialFile "/path/to/service_account.json" + + project_id "monitored_project" + + + +=over 4 + +=item B I + +Path to a JSON credentials file holding the credentials for a GCP service +account. + +If not specified, I. If running on GCE, +B may be set to chose a different service account associated with the +instance. + +=item B I + +The I or the I of the I. The +I is a string identifying the GCP project, which you can chose +freely when creating a new project. The I is a 12-digit decimal +number. You can look up both on the I. + +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 I + +Email address of an GCE I. This setting is only effective when +running on GCE and using I (see +B above). + +=item B I + +Configures the I to use when storing metrics. This option +takes a I and arbitrary string options which are used as labels. + +On GCE, defaults to the equivalent of this config: + + + project_id "${meta/project/project-id}" + instance_id "${meta/instance/id}" + zone "${meta/instance/zone}" + + +Where C<${meta/...}> are values read from the meta data service. + +When not running on GCE, defaults to the equivalent of this config: + + + project_id "${Project}" + + +Where C<${Project}> refers to the B option. + +See L for more information +on I. + +=item B I + +URL of the I API. Defaults to +C. + +=back + =head2 Plugin C 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 index 0c60ef0c..00000000 --- a/src/utils_format_gcm.c +++ /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 - **/ - -#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 -#include -#if HAVE_YAJL_YAJL_VERSION_H -#include -#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 index a43812c5..00000000 --- a/src/utils_format_gcm.h +++ /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 - **/ - -#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 index 52d6dc9b..00000000 --- a/src/utils_format_gcm_test.c +++ /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 - **/ - -#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 index 00000000..fda049c8 --- /dev/null +++ b/src/utils_format_stackdriver.c @@ -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 + **/ + +#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 +#include +#if HAVE_YAJL_YAJL_VERSION_H +#include +#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 index 00000000..fee260e3 --- /dev/null +++ b/src/utils_format_stackdriver.h @@ -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 + **/ + +#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 index 00000000..fa43866b --- /dev/null +++ b/src/utils_format_stackdriver_test.c @@ -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 + **/ + +#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 index 8ab1eec4..00000000 --- a/src/write_gcm.c +++ /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 - **/ - -#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 -#include - -/* - * 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 index 00000000..e6d3b74a --- /dev/null +++ b/src/write_stackdriver.c @@ -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 + **/ + +#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 +#include + +/* + * 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 : */