Merge remote-tracking branch 'github/pr/2472'
authorFlorian Forster <octo@collectd.org>
Thu, 18 Oct 2018 08:49:37 +0000 (10:49 +0200)
committerFlorian Forster <octo@collectd.org>
Thu, 18 Oct 2018 08:49:37 +0000 (10:49 +0200)
15 files changed:
Makefile.am
configure.ac
src/collectd.conf.in
src/collectd.conf.pod
src/daemon/utils_cache_mock.c
src/utils_format_kairosdb.c
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/utils_gce.c [new file with mode: 0644]
src/utils_gce.h [new file with mode: 0644]
src/utils_oauth.c [new file with mode: 0644]
src/utils_oauth.h [new file with mode: 0644]
src/utils_oauth_test.c [new file with mode: 0644]
src/write_stackdriver.c [new file with mode: 0644]

index 81a73d0..6a0f78e 100644 (file)
@@ -574,6 +574,68 @@ liboconfig_la_SOURCES = \
 liboconfig_la_CPPFLAGS = -I$(srcdir)/src/liboconfig $(AM_CPPFLAGS)
 liboconfig_la_LDFLAGS = -avoid-version $(LEXLIB)
 
+if BUILD_WITH_LIBCURL
+if BUILD_WITH_LIBSSL
+if BUILD_WITH_LIBYAJL2
+EXTRA_LTLIBRARIES += liboauth.la
+liboauth_la_SOURCES = \
+       src/utils_oauth.c \
+       src/utils_oauth.h
+liboauth_la_CPPFLAGS = \
+       $(AM_CPPFLAGS) \
+       $(BUILD_WITH_LIBCURL_CFLAGS) \
+       $(BUILD_WITH_LIBSSL_CFLAGS) \
+       $(BUILD_WITH_LIBYAJL_CPPFLAGS)
+liboauth_la_LIBADD = \
+       $(BUILD_WITH_LIBCURL_LIBS) \
+       $(BUILD_WITH_LIBSSL_LIBS) \
+       $(BUILD_WITH_LIBYAJL_LIBS)
+
+check_PROGRAMS += test_utils_oauth
+TESTS += test_utils_oauth
+test_utils_oauth_SOURCES = \
+       src/utils_oauth_test.c
+test_utils_oauth_LDADD = \
+       liboauth.la \
+       libcommon.la \
+       libplugin_mock.la
+
+EXTRA_LTLIBRARIES += libgce.la
+libgce_la_SOURCES = \
+       src/utils_gce.c \
+       src/utils_gce.h
+libgce_la_CPPFLAGS = \
+       $(AM_CPPFLAGS) \
+       $(BUILD_WITH_LIBCURL_CFLAGS)
+libgce_la_LIBADD = \
+       $(BUILD_WITH_LIBCURL_LIBS)
+endif
+endif
+endif
+
+if BUILD_WITH_LIBYAJL2
+EXTRA_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_stackdriver_la_LIBADD = \
+       libavltree.la \
+       $(BUILD_WITH_LIBSSL_LIBS) \
+       $(BUILD_WITH_LIBYAJL_LIBS)
+
+check_PROGRAMS += test_format_stackdriver
+TESTS += test_format_stackdriver
+test_format_stackdriver_SOURCES = \
+       src/utils_format_stackdriver_test.c \
+       src/testing.h
+test_format_stackdriver_LDADD = \
+       libformat_stackdriver.la \
+       libplugin_mock.la \
+       -lm
+endif
 
 if BUILD_PLUGIN_AGGREGATION
 pkglib_LTLIBRARIES += aggregation.la
@@ -1977,6 +2039,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 95caeff..4627965 100644 (file)
@@ -2304,6 +2304,8 @@ fi
 
 AC_SUBST(BUILD_WITH_LIBCURL_CFLAGS)
 AC_SUBST(BUILD_WITH_LIBCURL_LIBS)
+
+AM_CONDITIONAL([BUILD_WITH_LIBCURL], [test "x$with_libcurl" = "xyes"])
 # }}}
 
 # --with-libdbi {{{
@@ -5234,6 +5236,55 @@ PKG_CHECK_MODULES([LIBSIGROK], [libsigrok < 0.4],
 )
 # }}}
 
+# --with-libssl {{{
+with_libssl_cflags=""
+with_libssl_ldflags=""
+AC_ARG_WITH([libssl], [AS_HELP_STRING([--with-libssl@<:@=PREFIX@:>@], [Path to libssl.])],
+[
+       if test "x$withval" != "xno" && test "x$withval" != "xyes"; then
+               with_libssl_cppflags="-I$withval/include"
+               with_libssl_ldflags="-L$withval/lib"
+               with_libssl="yes"
+       else
+               with_libssl="$withval"
+       fi
+],
+[
+       with_libssl="yes"
+])
+if test "x$with_libssl" = "xyes"; then
+       SAVE_CPPFLAGS="$CPPFLAGS"
+       CPPFLAGS="$CPPFLAGS $with_libssl_cppflags"
+
+  AC_CHECK_HEADERS([openssl/sha.h openssl/blowfish.h openssl/rand.h],
+    [with_libssl="yes"],
+    [with_libssl="no (ssl header not found)"])
+
+       CPPFLAGS="$SAVE_CPPFLAGS"
+fi
+if test "x$with_libssl" = "xyes"; then
+       SAVE_CPPFLAGS="$CPPFLAGS"
+       SAVE_LDFLAGS="$LDFLAGS"
+       CPPFLAGS="$CPPFLAGS $with_libssl_cppflags"
+       LDFLAGS="$LDFLAGS $with_libssl_ldflags"
+
+       AC_CHECK_LIB([ssl], [OPENSSL_init_ssl], [with_libssl="yes"], [with_libssl="no (Symbol 'SSL_library_init' not found)"])
+
+       CPPFLAGS="$SAVE_CPPFLAGS"
+       LDFLAGS="$SAVE_LDFLAGS"
+fi
+if test "x$with_libssl" = "xyes"; then
+       BUILD_WITH_LIBSSL_CFLAGS="$with_libssl_cflags"
+       BUILD_WITH_LIBSSL_LDFLAGS="$with_libssl_ldflags"
+       BUILD_WITH_LIBSSL_LIBS="-lssl -lcrypto"
+       AC_SUBST([BUILD_WITH_LIBSSL_CFLAGS])
+       AC_SUBST([BUILD_WITH_LIBSSL_LDFLAGS])
+       AC_SUBST([BUILD_WITH_LIBSSL_LIBS])
+       AC_DEFINE([HAVE_LIBSSL], [1], [Define if libssl is present and usable.])
+fi
+AM_CONDITIONAL(BUILD_WITH_LIBSSL, test "x$with_libssl" = "xyes")
+# }}}
+
 # --with-libstatgrab {{{
 AC_ARG_WITH([libstatgrab],
   [AS_HELP_STRING([--with-libstatgrab@<:@=PREFIX@:>@], [Path to libstatgrab.])],
@@ -5789,6 +5840,7 @@ AC_SUBST([BUILD_WITH_LIBYAJL_LDFLAGS])
 AC_SUBST([BUILD_WITH_LIBYAJL_LIBS])
 
 AM_CONDITIONAL([BUILD_WITH_LIBYAJL], [test "x$with_libyajl" = "xyes"])
+AM_CONDITIONAL([BUILD_WITH_LIBYAJL2], [test "x$with_libyajl$with_libyajl2" = "xyesyes"])
 # }}}
 
 # --with-mic {{{
@@ -6350,6 +6402,7 @@ plugin_vmem="no"
 plugin_vserver="no"
 plugin_wireless="no"
 plugin_write_prometheus="no"
+plugin_write_stackdriver="no"
 plugin_xencpu="no"
 plugin_zfs_arc="no"
 plugin_zone="no"
@@ -6508,6 +6561,10 @@ if test "x$with_libcurl" = "xyes" && test "x$with_libyajl" = "xyes"; then
   plugin_curl_json="yes"
 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_stackdriver="yes"
+fi
+
 if test "x$with_libcurl" = "xyes" && test "x$with_libxml2" = "xyes"; then
   plugin_curl_xml="yes"
 fi
@@ -6696,163 +6753,164 @@ AC_ARG_ENABLE([all-plugins],
 
 m4_divert_once([HELP_ENABLE], [])
 
-AC_PLUGIN([aggregation],         [yes],                     [Aggregation plugin])
-AC_PLUGIN([amqp],                [$with_librabbitmq],       [AMQP output plugin])
-AC_PLUGIN([amqp1],               [$with_libqpid_proton],    [AMQP 1.0 output plugin])
-AC_PLUGIN([apache],              [$with_libcurl],           [Apache httpd statistics])
-AC_PLUGIN([apcups],              [yes],                     [Statistics of UPSes by APC])
-AC_PLUGIN([apple_sensors],       [$with_libiokit],          [Apple hardware sensors])
-AC_PLUGIN([aquaero],             [$with_libaquaero5],       [Aquaero hardware sensors])
-AC_PLUGIN([ascent],              [$plugin_ascent],          [AscentEmu player statistics])
-AC_PLUGIN([barometer],           [$plugin_barometer],       [Barometer sensor on I2C])
-AC_PLUGIN([battery],             [$plugin_battery],         [Battery statistics])
-AC_PLUGIN([bind],                [$plugin_bind],            [ISC Bind nameserver statistics])
-AC_PLUGIN([ceph],                [$plugin_ceph],            [Ceph daemon statistics])
-AC_PLUGIN([cgroups],             [$plugin_cgroups],         [CGroups CPU usage accounting])
-AC_PLUGIN([chrony],              [yes],                     [Chrony statistics])
-AC_PLUGIN([conntrack],           [$plugin_conntrack],       [nf_conntrack statistics])
-AC_PLUGIN([contextswitch],       [$plugin_contextswitch],   [context switch statistics])
-AC_PLUGIN([cpu],                 [$plugin_cpu],             [CPU usage statistics])
-AC_PLUGIN([cpufreq],             [$plugin_cpufreq],         [CPU frequency statistics])
-AC_PLUGIN([cpusleep],            [$plugin_cpusleep],        [CPU sleep statistics])
-AC_PLUGIN([csv],                 [yes],                     [CSV output plugin])
-AC_PLUGIN([curl],                [$with_libcurl],           [CURL generic web statistics])
-AC_PLUGIN([curl_json],           [$plugin_curl_json],       [CouchDB statistics])
-AC_PLUGIN([curl_xml],            [$plugin_curl_xml],        [CURL generic xml statistics])
-AC_PLUGIN([dbi],                 [$with_libdbi],            [General database statistics])
-AC_PLUGIN([df],                  [$plugin_df],              [Filesystem usage statistics])
-AC_PLUGIN([disk],                [$plugin_disk],            [Disk usage statistics])
-AC_PLUGIN([dns],                 [$with_libpcap],           [DNS traffic analysis])
-AC_PLUGIN([dpdkevents],          [$plugin_dpdkevents],      [Events from DPDK])
-AC_PLUGIN([dpdkstat],            [$plugin_dpdkstat],        [Stats from DPDK])
-AC_PLUGIN([drbd],                [$plugin_drbd],            [DRBD statistics])
-AC_PLUGIN([email],               [yes],                     [EMail statistics])
-AC_PLUGIN([entropy],             [$plugin_entropy],         [Entropy statistics])
-AC_PLUGIN([ethstat],             [$plugin_ethstat],         [Stats from NIC driver])
-AC_PLUGIN([exec],                [yes],                     [Execution of external programs])
-AC_PLUGIN([fhcount],             [$plugin_fhcount],         [File handles statistics])
-AC_PLUGIN([filecount],           [yes],                     [Count files in directories])
-AC_PLUGIN([fscache],             [$plugin_fscache],         [fscache statistics])
-AC_PLUGIN([gmond],               [$with_libganglia],        [Ganglia plugin])
-AC_PLUGIN([gps],                 [$plugin_gps],             [GPS plugin])
-AC_PLUGIN([grpc],                [$plugin_grpc],            [gRPC plugin])
-AC_PLUGIN([hddtemp],             [yes],                     [Query hddtempd])
-AC_PLUGIN([hugepages],           [$plugin_hugepages],       [Hugepages statistics])
-AC_PLUGIN([intel_pmu],           [$with_libjevents],        [Intel performance monitor plugin])
-AC_PLUGIN([intel_rdt],           [$with_libpqos],           [Intel RDT monitor plugin])
-AC_PLUGIN([interface],           [$plugin_interface],       [Interface traffic statistics])
-AC_PLUGIN([ipc],                 [$plugin_ipc],             [IPC statistics])
-AC_PLUGIN([ipmi],                [$plugin_ipmi],            [IPMI sensor statistics])
-AC_PLUGIN([iptables],            [$with_libiptc],           [IPTables rule counters])
-AC_PLUGIN([ipvs],                [$plugin_ipvs],            [IPVS connection statistics])
-AC_PLUGIN([irq],                 [$plugin_irq],             [IRQ statistics])
-AC_PLUGIN([java],                [$with_java],              [Embed the Java Virtual Machine])
-AC_PLUGIN([load],                [$plugin_load],            [System load])
-AC_PLUGIN([log_logstash],        [$plugin_log_logstash],    [Logstash json_event compatible logging])
-AC_PLUGIN([logfile],             [yes],                     [File logging plugin])
-AC_PLUGIN([lpar],                [$with_perfstat],          [AIX logical partitions statistics])
-AC_PLUGIN([lua],                 [$with_liblua],            [Lua plugin])
-AC_PLUGIN([lvm],                 [$with_liblvm2app],        [LVM statistics])
-AC_PLUGIN([madwifi],             [$have_linux_wireless_h],  [Madwifi wireless statistics])
-AC_PLUGIN([match_empty_counter], [yes],                     [The empty counter match])
-AC_PLUGIN([match_hashed],        [yes],                     [The hashed match])
-AC_PLUGIN([match_regex],         [yes],                     [The regex match])
-AC_PLUGIN([match_timediff],      [yes],                     [The timediff match])
-AC_PLUGIN([match_value],         [yes],                     [The value match])
-AC_PLUGIN([mbmon],               [yes],                     [Query mbmond])
-AC_PLUGIN([mcelog],              [$plugin_mcelog],          [Machine Check Exceptions notifications])
-AC_PLUGIN([md],                  [$have_linux_raid_md_u_h], [md (Linux software RAID) devices])
-AC_PLUGIN([memcachec],           [$with_libmemcached],      [memcachec statistics])
-AC_PLUGIN([memcached],           [yes],                     [memcached statistics])
-AC_PLUGIN([memory],              [$plugin_memory],          [Memory usage])
-AC_PLUGIN([mic],                 [$with_mic],               [Intel Many Integrated Core stats])
-AC_PLUGIN([modbus],              [$with_libmodbus],         [Modbus plugin])
-AC_PLUGIN([mqtt],                [$with_libmosquitto],      [MQTT output plugin])
-AC_PLUGIN([multimeter],          [$plugin_multimeter],      [Read multimeter values])
-AC_PLUGIN([mysql],               [$with_libmysql],          [MySQL statistics])
-AC_PLUGIN([netapp],              [$with_libnetapp],         [NetApp plugin])
-AC_PLUGIN([netlink],             [$with_libmnl],            [Enhanced Linux network statistics])
-AC_PLUGIN([network],             [yes],                     [Network communication plugin])
-AC_PLUGIN([nfs],                 [$plugin_nfs],             [NFS statistics])
-AC_PLUGIN([nginx],               [$with_libcurl],           [nginx statistics])
-AC_PLUGIN([notify_desktop],      [$with_libnotify],         [Desktop notifications])
-AC_PLUGIN([notify_email],        [$with_libesmtp],          [Email notifier])
-AC_PLUGIN([notify_nagios],       [yes],                     [Nagios notification plugin])
-AC_PLUGIN([ntpd],                [yes],                     [NTPd statistics])
-AC_PLUGIN([numa],                [$plugin_numa],            [NUMA virtual memory statistics])
-AC_PLUGIN([nut],                 [$with_libupsclient],      [Network UPS tools statistics])
-AC_PLUGIN([olsrd],               [yes],                     [olsrd statistics])
-AC_PLUGIN([onewire],             [$with_libowcapi],         [OneWire sensor statistics])
-AC_PLUGIN([openldap],            [$with_libldap],           [OpenLDAP statistics])
-AC_PLUGIN([openvpn],             [yes],                     [OpenVPN client statistics])
-AC_PLUGIN([oracle],              [$with_oracle],            [Oracle plugin])
-AC_PLUGIN([ovs_events],          [$plugin_ovs_events],      [OVS events plugin])
-AC_PLUGIN([ovs_stats],           [$plugin_ovs_stats],       [OVS statistics plugin])
-AC_PLUGIN([pcie_errors],         [$plugin_pcie_errors],     [PCIe errors plugin])
-AC_PLUGIN([perl],                [$plugin_perl],            [Embed a Perl interpreter])
-AC_PLUGIN([pf],                  [$have_net_pfvar_h],       [BSD packet filter (PF) statistics])
+AC_PLUGIN([aggregation],         [yes],                       [Aggregation plugin])
+AC_PLUGIN([amqp],                [$with_librabbitmq],         [AMQP output plugin])
+AC_PLUGIN([amqp1],               [$with_libqpid_proton],      [AMQP 1.0 output plugin])
+AC_PLUGIN([apache],              [$with_libcurl],             [Apache httpd statistics])
+AC_PLUGIN([apcups],              [yes],                       [Statistics of UPSes by APC])
+AC_PLUGIN([apple_sensors],       [$with_libiokit],            [Apple hardware sensors])
+AC_PLUGIN([aquaero],             [$with_libaquaero5],         [Aquaero hardware sensors])
+AC_PLUGIN([ascent],              [$plugin_ascent],            [AscentEmu player statistics])
+AC_PLUGIN([barometer],           [$plugin_barometer],         [Barometer sensor on I2C])
+AC_PLUGIN([battery],             [$plugin_battery],           [Battery statistics])
+AC_PLUGIN([bind],                [$plugin_bind],              [ISC Bind nameserver statistics])
+AC_PLUGIN([ceph],                [$plugin_ceph],              [Ceph daemon statistics])
+AC_PLUGIN([cgroups],             [$plugin_cgroups],           [CGroups CPU usage accounting])
+AC_PLUGIN([chrony],              [yes],                       [Chrony statistics])
+AC_PLUGIN([conntrack],           [$plugin_conntrack],         [nf_conntrack statistics])
+AC_PLUGIN([contextswitch],       [$plugin_contextswitch],     [context switch statistics])
+AC_PLUGIN([cpu],                 [$plugin_cpu],               [CPU usage statistics])
+AC_PLUGIN([cpufreq],             [$plugin_cpufreq],           [CPU frequency statistics])
+AC_PLUGIN([cpusleep],            [$plugin_cpusleep],          [CPU sleep statistics])
+AC_PLUGIN([csv],                 [yes],                       [CSV output plugin])
+AC_PLUGIN([curl],                [$with_libcurl],             [CURL generic web statistics])
+AC_PLUGIN([curl_json],           [$plugin_curl_json],         [CouchDB statistics])
+AC_PLUGIN([curl_xml],            [$plugin_curl_xml],          [CURL generic xml statistics])
+AC_PLUGIN([dbi],                 [$with_libdbi],              [General database statistics])
+AC_PLUGIN([df],                  [$plugin_df],                [Filesystem usage statistics])
+AC_PLUGIN([disk],                [$plugin_disk],              [Disk usage statistics])
+AC_PLUGIN([dns],                 [$with_libpcap],             [DNS traffic analysis])
+AC_PLUGIN([dpdkevents],          [$plugin_dpdkevents],        [Events from DPDK])
+AC_PLUGIN([dpdkstat],            [$plugin_dpdkstat],          [Stats from DPDK])
+AC_PLUGIN([drbd],                [$plugin_drbd],              [DRBD statistics])
+AC_PLUGIN([email],               [yes],                       [EMail statistics])
+AC_PLUGIN([entropy],             [$plugin_entropy],           [Entropy statistics])
+AC_PLUGIN([ethstat],             [$plugin_ethstat],           [Stats from NIC driver])
+AC_PLUGIN([exec],                [yes],                       [Execution of external programs])
+AC_PLUGIN([fhcount],             [$plugin_fhcount],           [File handles statistics])
+AC_PLUGIN([filecount],           [yes],                       [Count files in directories])
+AC_PLUGIN([fscache],             [$plugin_fscache],           [fscache statistics])
+AC_PLUGIN([gmond],               [$with_libganglia],          [Ganglia plugin])
+AC_PLUGIN([gps],                 [$plugin_gps],               [GPS plugin])
+AC_PLUGIN([grpc],                [$plugin_grpc],              [gRPC plugin])
+AC_PLUGIN([hddtemp],             [yes],                       [Query hddtempd])
+AC_PLUGIN([hugepages],           [$plugin_hugepages],         [Hugepages statistics])
+AC_PLUGIN([intel_pmu],           [$with_libjevents],          [Intel performance monitor plugin])
+AC_PLUGIN([intel_rdt],           [$with_libpqos],             [Intel RDT monitor plugin])
+AC_PLUGIN([interface],           [$plugin_interface],         [Interface traffic statistics])
+AC_PLUGIN([ipc],                 [$plugin_ipc],               [IPC statistics])
+AC_PLUGIN([ipmi],                [$plugin_ipmi],              [IPMI sensor statistics])
+AC_PLUGIN([iptables],            [$with_libiptc],             [IPTables rule counters])
+AC_PLUGIN([ipvs],                [$plugin_ipvs],              [IPVS connection statistics])
+AC_PLUGIN([irq],                 [$plugin_irq],               [IRQ statistics])
+AC_PLUGIN([java],                [$with_java],                [Embed the Java Virtual Machine])
+AC_PLUGIN([load],                [$plugin_load],              [System load])
+AC_PLUGIN([log_logstash],        [$plugin_log_logstash],      [Logstash json_event compatible logging])
+AC_PLUGIN([logfile],             [yes],                       [File logging plugin])
+AC_PLUGIN([lpar],                [$with_perfstat],            [AIX logical partitions statistics])
+AC_PLUGIN([lua],                 [$with_liblua],              [Lua plugin])
+AC_PLUGIN([lvm],                 [$with_liblvm2app],          [LVM statistics])
+AC_PLUGIN([madwifi],             [$have_linux_wireless_h],    [Madwifi wireless statistics])
+AC_PLUGIN([match_empty_counter], [yes],                       [The empty counter match])
+AC_PLUGIN([match_hashed],        [yes],                       [The hashed match])
+AC_PLUGIN([match_regex],         [yes],                       [The regex match])
+AC_PLUGIN([match_timediff],      [yes],                       [The timediff match])
+AC_PLUGIN([match_value],         [yes],                       [The value match])
+AC_PLUGIN([mbmon],               [yes],                       [Query mbmond])
+AC_PLUGIN([mcelog],              [$plugin_mcelog],            [Machine Check Exceptions notifications])
+AC_PLUGIN([md],                  [$have_linux_raid_md_u_h],   [md (Linux software RAID) devices])
+AC_PLUGIN([memcachec],           [$with_libmemcached],        [memcachec statistics])
+AC_PLUGIN([memcached],           [yes],                       [memcached statistics])
+AC_PLUGIN([memory],              [$plugin_memory],            [Memory usage])
+AC_PLUGIN([mic],                 [$with_mic],                 [Intel Many Integrated Core stats])
+AC_PLUGIN([modbus],              [$with_libmodbus],           [Modbus plugin])
+AC_PLUGIN([mqtt],                [$with_libmosquitto],        [MQTT output plugin])
+AC_PLUGIN([multimeter],          [$plugin_multimeter],        [Read multimeter values])
+AC_PLUGIN([mysql],               [$with_libmysql],            [MySQL statistics])
+AC_PLUGIN([netapp],              [$with_libnetapp],           [NetApp plugin])
+AC_PLUGIN([netlink],             [$with_libmnl],              [Enhanced Linux network statistics])
+AC_PLUGIN([network],             [yes],                       [Network communication plugin])
+AC_PLUGIN([nfs],                 [$plugin_nfs],               [NFS statistics])
+AC_PLUGIN([nginx],               [$with_libcurl],             [nginx statistics])
+AC_PLUGIN([notify_desktop],      [$with_libnotify],           [Desktop notifications])
+AC_PLUGIN([notify_email],        [$with_libesmtp],            [Email notifier])
+AC_PLUGIN([notify_nagios],       [yes],                       [Nagios notification plugin])
+AC_PLUGIN([ntpd],                [yes],                       [NTPd statistics])
+AC_PLUGIN([numa],                [$plugin_numa],              [NUMA virtual memory statistics])
+AC_PLUGIN([nut],                 [$with_libupsclient],        [Network UPS tools statistics])
+AC_PLUGIN([olsrd],               [yes],                       [olsrd statistics])
+AC_PLUGIN([onewire],             [$with_libowcapi],           [OneWire sensor statistics])
+AC_PLUGIN([openldap],            [$with_libldap],             [OpenLDAP statistics])
+AC_PLUGIN([openvpn],             [yes],                       [OpenVPN client statistics])
+AC_PLUGIN([oracle],              [$with_oracle],              [Oracle plugin])
+AC_PLUGIN([ovs_events],          [$plugin_ovs_events],        [OVS events plugin])
+AC_PLUGIN([ovs_stats],           [$plugin_ovs_stats],         [OVS statistics plugin])
+AC_PLUGIN([pcie_errors],         [$plugin_pcie_errors],       [PCIe errors plugin])
+AC_PLUGIN([perl],                [$plugin_perl],              [Embed a Perl interpreter])
+AC_PLUGIN([pf],                  [$have_net_pfvar_h],         [BSD packet filter (PF) statistics])
 # FIXME: Check for libevent, too.
-AC_PLUGIN([pinba],               [$plugin_pinba],           [Pinba statistics])
-AC_PLUGIN([ping],                [$with_liboping],          [Network latency statistics])
-AC_PLUGIN([postgresql],          [$with_libpq],             [PostgreSQL database statistics])
-AC_PLUGIN([powerdns],            [yes],                     [PowerDNS statistics])
-AC_PLUGIN([processes],           [$plugin_processes],       [Process statistics])
-AC_PLUGIN([protocols],           [$plugin_protocols],       [Protocol (IP, TCP, ...) statistics])
-AC_PLUGIN([python],              [$plugin_python],          [Embed a Python interpreter])
-AC_PLUGIN([redis],               [$with_libhiredis],        [Redis plugin])
-AC_PLUGIN([routeros],            [$with_librouteros],       [RouterOS plugin])
-AC_PLUGIN([rrdcached],           [$librrd_rrdc_update],     [RRDTool output plugin])
-AC_PLUGIN([rrdtool],             [$with_librrd],            [RRDTool output plugin])
-AC_PLUGIN([sensors],             [$with_libsensors],        [lm_sensors statistics])
-AC_PLUGIN([serial],              [$plugin_serial],          [serial port traffic])
-AC_PLUGIN([sigrok],              [$with_libsigrok],         [sigrok acquisition sources])
-AC_PLUGIN([smart],               [$plugin_smart],           [SMART statistics])
-AC_PLUGIN([snmp],                [$with_libnetsnmp],        [SNMP querying plugin])
-AC_PLUGIN([snmp_agent],          [$with_libnetsnmpagent],   [SNMP agent plugin])
-AC_PLUGIN([statsd],              [yes],                     [StatsD plugin])
-AC_PLUGIN([swap],                [$plugin_swap],            [Swap usage statistics])
-AC_PLUGIN([synproxy],            [$plugin_synproxy],        [Synproxy stats plugin])
-AC_PLUGIN([syslog],              [$have_syslog],            [Syslog logging plugin])
-AC_PLUGIN([table],               [yes],                     [Parsing of tabular data])
-AC_PLUGIN([tail],                [yes],                     [Parsing of logfiles])
-AC_PLUGIN([tail_csv],            [yes],                     [Parsing of CSV files])
-AC_PLUGIN([tape],                [$plugin_tape],            [Tape drive statistics])
-AC_PLUGIN([target_notification], [yes],                     [The notification target])
-AC_PLUGIN([target_replace],      [yes],                     [The replace target])
-AC_PLUGIN([target_scale],        [yes],                     [The scale target])
-AC_PLUGIN([target_set],          [yes],                     [The set target])
-AC_PLUGIN([target_v5upgrade],    [yes],                     [The v5upgrade target])
-AC_PLUGIN([tcpconns],            [$plugin_tcpconns],        [TCP connection statistics])
-AC_PLUGIN([teamspeak2],          [yes],                     [TeamSpeak2 server statistics])
-AC_PLUGIN([ted],                 [$plugin_ted],             [Read The Energy Detective values])
-AC_PLUGIN([thermal],             [$plugin_thermal],         [Linux ACPI thermal zone statistics])
-AC_PLUGIN([threshold],           [yes],                     [Threshold checking plugin])
-AC_PLUGIN([tokyotyrant],         [$with_libtokyotyrant],    [TokyoTyrant database statistics])
-AC_PLUGIN([turbostat],           [$plugin_turbostat],       [Advanced statistic on Intel cpu states])
-AC_PLUGIN([unixsock],            [yes],                     [Unixsock communication plugin])
-AC_PLUGIN([uptime],              [$plugin_uptime],          [Uptime statistics])
-AC_PLUGIN([users],               [$plugin_users],           [User statistics])
-AC_PLUGIN([uuid],                [yes],                     [UUID as hostname plugin])
-AC_PLUGIN([varnish],             [$with_libvarnish],        [Varnish cache statistics])
-AC_PLUGIN([virt],                [$plugin_virt],            [Virtual machine statistics])
-AC_PLUGIN([vmem],                [$plugin_vmem],            [Virtual memory statistics])
-AC_PLUGIN([vserver],             [$plugin_vserver],         [Linux VServer statistics])
-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_kafka],         [$with_librdkafka],        [Kafka output plugin])
-AC_PLUGIN([write_log],           [yes],                     [Log output plugin])
-AC_PLUGIN([write_mongodb],       [$with_libmongoc],         [MongoDB output plugin])
-AC_PLUGIN([write_prometheus],    [$plugin_write_prometheus], [Prometheus write plugin])
-AC_PLUGIN([write_redis],         [$with_libhiredis],        [Redis output plugin])
-AC_PLUGIN([write_riemann],       [$with_libriemann_client], [Riemann output plugin])
-AC_PLUGIN([write_sensu],         [yes],                     [Sensu output plugin])
-AC_PLUGIN([write_tsdb],          [yes],                     [TSDB output plugin])
-AC_PLUGIN([xencpu],              [$plugin_xencpu],          [Xen Host CPU usage])
-AC_PLUGIN([xmms],                [$with_libxmms],           [XMMS statistics])
-AC_PLUGIN([zfs_arc],             [$plugin_zfs_arc],         [ZFS ARC statistics])
-AC_PLUGIN([zone],                [$plugin_zone],            [Solaris container statistics])
-AC_PLUGIN([zookeeper],           [yes],                     [Zookeeper statistics])
+AC_PLUGIN([pinba],               [$plugin_pinba],             [Pinba statistics])
+AC_PLUGIN([ping],                [$with_liboping],            [Network latency statistics])
+AC_PLUGIN([postgresql],          [$with_libpq],               [PostgreSQL database statistics])
+AC_PLUGIN([powerdns],            [yes],                       [PowerDNS statistics])
+AC_PLUGIN([processes],           [$plugin_processes],         [Process statistics])
+AC_PLUGIN([protocols],           [$plugin_protocols],         [Protocol (IP, TCP, ...) statistics])
+AC_PLUGIN([python],              [$plugin_python],            [Embed a Python interpreter])
+AC_PLUGIN([redis],               [$with_libhiredis],          [Redis plugin])
+AC_PLUGIN([routeros],            [$with_librouteros],         [RouterOS plugin])
+AC_PLUGIN([rrdcached],           [$librrd_rrdc_update],       [RRDTool output plugin])
+AC_PLUGIN([rrdtool],             [$with_librrd],              [RRDTool output plugin])
+AC_PLUGIN([sensors],             [$with_libsensors],          [lm_sensors statistics])
+AC_PLUGIN([serial],              [$plugin_serial],            [serial port traffic])
+AC_PLUGIN([sigrok],              [$with_libsigrok],           [sigrok acquisition sources])
+AC_PLUGIN([smart],               [$plugin_smart],             [SMART statistics])
+AC_PLUGIN([snmp],                [$with_libnetsnmp],          [SNMP querying plugin])
+AC_PLUGIN([snmp_agent],          [$with_libnetsnmpagent],     [SNMP agent plugin])
+AC_PLUGIN([statsd],              [yes],                       [StatsD plugin])
+AC_PLUGIN([swap],                [$plugin_swap],              [Swap usage statistics])
+AC_PLUGIN([synproxy],            [$plugin_synproxy],          [Synproxy stats plugin])
+AC_PLUGIN([syslog],              [$have_syslog],              [Syslog logging plugin])
+AC_PLUGIN([table],               [yes],                       [Parsing of tabular data])
+AC_PLUGIN([tail],                [yes],                       [Parsing of logfiles])
+AC_PLUGIN([tail_csv],            [yes],                       [Parsing of CSV files])
+AC_PLUGIN([tape],                [$plugin_tape],              [Tape drive statistics])
+AC_PLUGIN([target_notification], [yes],                       [The notification target])
+AC_PLUGIN([target_replace],      [yes],                       [The replace target])
+AC_PLUGIN([target_scale],        [yes],                       [The scale target])
+AC_PLUGIN([target_set],          [yes],                       [The set target])
+AC_PLUGIN([target_v5upgrade],    [yes],                       [The v5upgrade target])
+AC_PLUGIN([tcpconns],            [$plugin_tcpconns],          [TCP connection statistics])
+AC_PLUGIN([teamspeak2],          [yes],                       [TeamSpeak2 server statistics])
+AC_PLUGIN([ted],                 [$plugin_ted],               [Read The Energy Detective values])
+AC_PLUGIN([thermal],             [$plugin_thermal],           [Linux ACPI thermal zone statistics])
+AC_PLUGIN([threshold],           [yes],                       [Threshold checking plugin])
+AC_PLUGIN([tokyotyrant],         [$with_libtokyotyrant],      [TokyoTyrant database statistics])
+AC_PLUGIN([turbostat],           [$plugin_turbostat],         [Advanced statistic on Intel cpu states])
+AC_PLUGIN([unixsock],            [yes],                       [Unixsock communication plugin])
+AC_PLUGIN([uptime],              [$plugin_uptime],            [Uptime statistics])
+AC_PLUGIN([users],               [$plugin_users],             [User statistics])
+AC_PLUGIN([uuid],                [yes],                       [UUID as hostname plugin])
+AC_PLUGIN([varnish],             [$with_libvarnish],          [Varnish cache statistics])
+AC_PLUGIN([virt],                [$plugin_virt],              [Virtual machine statistics])
+AC_PLUGIN([vmem],                [$plugin_vmem],              [Virtual memory statistics])
+AC_PLUGIN([vserver],             [$plugin_vserver],           [Linux VServer statistics])
+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_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])
+AC_PLUGIN([write_prometheus],    [$plugin_write_prometheus],  [Prometheus write plugin])
+AC_PLUGIN([write_redis],         [$with_libhiredis],          [Redis output plugin])
+AC_PLUGIN([write_riemann],       [$with_libriemann_client],   [Riemann output plugin])
+AC_PLUGIN([write_sensu],         [yes],                       [Sensu output plugin])
+AC_PLUGIN([write_tsdb],          [yes],                       [TSDB output plugin])
+AC_PLUGIN([xencpu],              [$plugin_xencpu],            [Xen Host CPU usage])
+AC_PLUGIN([xmms],                [$with_libxmms],             [XMMS statistics])
+AC_PLUGIN([zfs_arc],             [$plugin_zfs_arc],           [ZFS ARC statistics])
+AC_PLUGIN([zone],                [$plugin_zone],              [Solaris container statistics])
+AC_PLUGIN([zookeeper],           [yes],                       [Zookeeper statistics])
 
 dnl Default configuration file
 # Load either syslog or logfile
@@ -7096,6 +7154,7 @@ AC_MSG_RESULT([    librouteros . . . . . $with_librouteros])
 AC_MSG_RESULT([    librrd  . . . . . . . $with_librrd])
 AC_MSG_RESULT([    libsensors  . . . . . $with_libsensors])
 AC_MSG_RESULT([    libsigrok   . . . . . $with_libsigrok])
+AC_MSG_RESULT([    libssl  . . . . . . . $with_libssl])
 AC_MSG_RESULT([    libstatgrab . . . . . $with_libstatgrab])
 AC_MSG_RESULT([    libtokyotyrant  . . . $with_libtokyotyrant])
 AC_MSG_RESULT([    libudev . . . . . . . $with_libudev])
@@ -7268,6 +7327,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 af65214..9421475 100644 (file)
 #@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
 #      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">
+#    Label "project_id" "gcp-project-id"
+#  </Resource>
+#  Url "https://monitoring.googleapis.com/v3"
+#</Plugin>
+
 #<Plugin write_tsdb>
 #      <Node>
 #              Host "localhost"
index b1f8eb4..e11e514 100644 (file)
@@ -10342,6 +10342,133 @@ 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">
+     Label "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 B<CredentialFile> is not specified, the plugin uses I<Application Default
+Credentials>. That means which credentials are used depends on the environment:
+
+=over 4
+
+=item
+
+The environment variable C<GOOGLE_APPLICATION_CREDENTIALS> is checked. If this
+variable is specified it should point to a JSON file that defines the
+credentials.
+
+=item
+
+The path C<${HOME}/.config/gcloud/application_default_credentials.json> is
+checked. This where credentials used by the I<gcloud> command line utility are
+stored. You can use C<gcloud auth application-default login> to create these
+credentials.
+
+Please note that these credentials are often of your personal account, not a
+service account, and are therefore unfit to be used in a production
+environment.
+
+=item
+
+When running on GCE, the built-in service account associated with the virtual
+machine instance is used.
+See also the B<Email> option below.
+
+=back
+
+=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> (GCE only)
+
+Choses the GCE I<Service Account> used for authentication.
+
+Each GCE instance has a C<default> I<Service Account> but may also be
+associated with additional I<Service Accounts>. This is often used to restrict
+the permissions of services running on the GCE instance to the required
+minimum. The I<write_stackdriver plugin> requires the
+C<https://www.googleapis.com/auth/monitoring> scope. When multiple I<Service
+Accounts> are available, this option selects which one is used by
+I<write_stackdriver plugin>.
+
+=item B<Resource> I<ResourceType>
+
+Configures the I<Monitored Resource> to use when storing metrics.
+More information on I<Monitored Resources> and I<Monitored Resource Types> are
+available at L<https://cloud.google.com/monitoring/api/resources>.
+
+This block takes one string argument, the I<ResourceType>. Inside the block are
+one or more B<Label> options which configure the resource labels.
+
+This block is optional. The default value depends on the runtime environment:
+on GCE, the C<gce_instance> resource type is used, otherwise the C<global>
+resource type ist used:
+
+=over 4
+
+=item
+
+B<On GCE>, defaults to the equivalent of this config:
+
+  <Resource "gce_instance">
+    Label "project_id" "<project_id>"
+    Label "instance_id" "<instance_id>"
+    Label "zone" "<zone>"
+  </Resource>
+
+The values for I<project_id>, I<instance_id> and I<zone> are read from the GCE
+metadata service.
+
+=item
+
+B<Elsewhere>, i.e. not on GCE, defaults to the equivalent of this config:
+
+  <Resource "global">
+    Label "project_id" "<Project>"
+  </Resource>
+
+Where I<Project> refers to the value of the B<Project> option or the project ID
+inferred from the B<CredentialFile>.
+
+=back
+
+=item B<Url> I<Url>
+
+URL of the I<Stackdriver 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
index 1495a80..0b8c997 100644 (file)
@@ -27,6 +27,8 @@
 #include "utils_cache.h"
 #include <errno.h>
 
+#include <errno.h>
+
 gauge_t *uc_get_rate(__attribute__((unused)) data_set_t const *ds,
                      __attribute__((unused)) value_list_t const *vl) {
   errno = ENOTSUP;
@@ -46,3 +48,13 @@ int uc_get_value_by_name(const char *name, value_t **ret_values,
                          size_t *ret_values_num) {
   return ENOTSUP;
 }
+
+int uc_meta_data_get_unsigned_int(const value_list_t *vl, const char *key,
+                                  uint64_t *value) {
+  return -ENOENT;
+}
+
+int uc_meta_data_add_unsigned_int(const value_list_t *vl, const char *key,
+                                  uint64_t value) {
+  return 0;
+}
index 4003243..4998906 100644 (file)
@@ -356,5 +356,3 @@ int format_kairosdb_value_list(char *buffer, /* {{{ */
       (*ret_buffer_free) - 2, http_attrs, http_attrs_num, data_ttl,
       metrics_prefix);
 } /* }}} int format_kairosdb_value_list */
-
-/* vim: set sw=2 sts=2 et fdm=marker : */
diff --git a/src/utils_format_stackdriver.c b/src/utils_format_stackdriver.c
new file mode 100644 (file)
index 0000000..afaa8ed
--- /dev/null
@@ -0,0 +1,766 @@
+/**
+ * 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 = 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) /* {{{ */
+{
+  yajl_gen_map_open(gen);
+
+  int status = json_string(gen, "type") || json_string(gen, res->type);
+  if (status != 0)
+    return status;
+
+  if (res->labels_num != 0) {
+    status = json_string(gen, "labels");
+    if (status != 0)
+      return status;
+
+    yajl_gen_map_open(gen);
+    for (size_t 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_typed_value(yajl_gen gen, int ds_type, value_t v,
+                              int64_t start_value) {
+  char integer[32];
+
+  yajl_gen_map_open(gen);
+
+  switch (ds_type) {
+  case DS_TYPE_GAUGE: {
+    int 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;
+
+    yajl_gen_map_close(gen);
+    return 0;
+  }
+  case DS_TYPE_DERIVE: {
+    derive_t diff = v.derive - (derive_t)start_value;
+    snprintf(integer, sizeof(integer), "%" PRIi64, diff);
+    break;
+  }
+  case DS_TYPE_COUNTER: {
+    counter_t diff = counter_diff((counter_t)start_value, v.counter);
+    snprintf(integer, sizeof(integer), "%llu", diff);
+    break;
+  }
+  case DS_TYPE_ABSOLUTE: {
+    snprintf(integer, sizeof(integer), "%" PRIu64, v.absolute);
+    break;
+  }
+  default: {
+    ERROR("format_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_typed_value */
+
+/* MetricKind
+ *
+ * enum(
+ *   "CUMULATIVE",
+ *   "GAUGE"
+ * )
+*/
+static int format_metric_kind(yajl_gen gen, int ds_type) {
+  switch (ds_type) {
+  case DS_TYPE_GAUGE:
+  case DS_TYPE_ABSOLUTE:
+    return json_string(gen, "GAUGE");
+  case DS_TYPE_COUNTER:
+  case DS_TYPE_DERIVE:
+    return json_string(gen, "CUMULATIVE");
+  default:
+    ERROR("format_metric_kind: unknown value type %d.", ds_type);
+    return EINVAL;
+  }
+}
+
+/* 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, cdtime_t start_time) {
+  /* {{{ */
+  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_DERIVE) || (ds_type == DS_TYPE_COUNTER)) {
+    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 */
+
+/* read_cumulative_state reads the start time and start value of cumulative
+ * (i.e. DERIVE or COUNTER) metrics from the cache. If a metric is seen for the
+ * first time, or when a DERIVE metric is reset, the start time is (re)set to
+ * vl->time. */
+static int read_cumulative_state(data_set_t const *ds, value_list_t const *vl,
+                                 int ds_index, cdtime_t *ret_start_time,
+                                 int64_t *ret_start_value) {
+  int ds_type = ds->ds[ds_index].type;
+  if ((ds_type != DS_TYPE_DERIVE) && (ds_type != DS_TYPE_COUNTER)) {
+    return 0;
+  }
+
+  char start_value_key[DATA_MAX_NAME_LEN];
+  snprintf(start_value_key, sizeof(start_value_key),
+           "stackdriver:start_value[%d]", ds_index);
+
+  int status =
+      uc_meta_data_get_signed_int(vl, start_value_key, ret_start_value);
+  if ((status == 0) && ((ds_type != DS_TYPE_DERIVE) ||
+                        (*ret_start_value <= vl->values[ds_index].derive))) {
+    return uc_meta_data_get_unsigned_int(vl, "stackdriver:start_time",
+                                         ret_start_time);
+  }
+
+  if (ds_type == DS_TYPE_DERIVE) {
+    *ret_start_value = vl->values[ds_index].derive;
+  } else {
+    *ret_start_value = (int64_t)vl->values[ds_index].counter;
+  }
+  *ret_start_time = vl->time;
+
+  status = uc_meta_data_add_signed_int(vl, start_value_key, *ret_start_value);
+  if (status != 0) {
+    return status;
+  }
+  return uc_meta_data_add_unsigned_int(vl, "stackdriver:start_time",
+                                       *ret_start_time);
+} /* int read_cumulative_state */
+
+/* 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,
+                        cdtime_t start_time, int64_t start_value) {
+  /* {{{ */
+  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, start_time) ||
+      json_string(gen, "value") ||
+      format_typed_value(gen, ds_type, vl->values[ds_index], start_value);
+  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)
+ *     }
+ *   ],
+ * }
+ */
+/* format_time_series formats a TimeSeries object. Returns EAGAIN when a
+ * cumulative metric is seen for the first time and cannot be sent to
+ * Stackdriver due to lack of state. */
+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) {
+  int ds_type = ds->ds[ds_index].type;
+
+  cdtime_t start_time = 0;
+  int64_t start_value = 0;
+  int status =
+      read_cumulative_state(ds, vl, ds_index, &start_time, &start_value);
+  if (status != 0) {
+    return status;
+  }
+  if (start_time == vl->time) {
+    /* for cumulative metrics, the interval must not be zero. */
+    return EAGAIN;
+  }
+
+  yajl_gen_map_open(gen);
+
+  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);
+
+  status = format_point(gen, ds, vl, ds_index, start_time, start_value);
+  if (status != 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) /* {{{ */
+{
+  /* 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;
+    }
+  }
+
+  char key[6 * DATA_MAX_NAME_LEN];
+  int 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;
+  }
+
+  _Bool staged = 0;
+  for (size_t i = 0; i < ds->ds_num; i++) {
+    int status = format_time_series(out->gen, ds, vl, i, out->res);
+    if (status == EAGAIN) {
+      /* first instance of a cumulative metric */
+      continue;
+    }
+    if (status != 0) {
+      ERROR("sd_output_add: format_time_series failed with status %d.", status);
+      return status;
+    }
+    staged = 1;
+  }
+
+  if (staged) {
+    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) /* {{{ */
+{
+  sd_output_finalize(out);
+
+  unsigned char const *json_buffer = NULL;
+  yajl_gen_get_buf(out->gen, &json_buffer, &(size_t){0});
+  char *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 = 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) /* {{{ */
+{
+  if (res == NULL)
+    return;
+
+  for (size_t 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) /* {{{ */
+{
+  if ((res == NULL) || (key == NULL) || (value == NULL))
+    return EINVAL;
+
+  sd_label_t *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 */
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/utils_gce.c b/src/utils_gce.c
new file mode 100644 (file)
index 0000000..d43d1de
--- /dev/null
@@ -0,0 +1,284 @@
+/**
+ * collectd - src/utils_gce.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 "plugin.h"
+#include "utils_gce.h"
+#include "utils_oauth.h"
+#include "utils_time.h"
+
+#include <curl/curl.h>
+
+#ifndef GCP_METADATA_PREFIX
+#define GCP_METADATA_PREFIX "http://metadata.google.internal/computeMetadata/v1"
+#endif
+#ifndef GCE_METADATA_HEADER
+#define GCE_METADATA_HEADER "Metadata-Flavor: Google"
+#endif
+
+#ifndef GCE_INSTANCE_ID_URL
+#define GCE_INSTANCE_ID_URL GCP_METADATA_PREFIX "/instance/id"
+#endif
+#ifndef GCE_PROJECT_NUM_URL
+#define GCE_PROJECT_NUM_URL GCP_METADATA_PREFIX "/project/numeric-project-id"
+#endif
+#ifndef GCE_PROJECT_ID_URL
+#define GCE_PROJECT_ID_URL GCP_METADATA_PREFIX "/project/project-id"
+#endif
+#ifndef GCE_ZONE_URL
+#define GCE_ZONE_URL GCP_METADATA_PREFIX "/instance/zone"
+#endif
+
+#ifndef GCE_DEFAULT_SERVICE_ACCOUNT
+#define GCE_DEFAULT_SERVICE_ACCOUNT "default"
+#endif
+
+#ifndef GCE_SCOPE_URL
+#define GCE_SCOPE_URL_FORMAT                                                   \
+  GCP_METADATA_PREFIX "/instance/service-accounts/%s/scopes"
+#endif
+#ifndef GCE_TOKEN_URL
+#define GCE_TOKEN_URL_FORMAT                                                   \
+  GCP_METADATA_PREFIX "/instance/service-accounts/%s/token"
+#endif
+
+struct blob_s {
+  char *data;
+  size_t size;
+};
+typedef struct blob_s blob_t;
+
+static int on_gce = -1;
+
+static char *token = NULL;
+static char *token_email = NULL;
+static cdtime_t token_valid_until = 0;
+static pthread_mutex_t token_lock = PTHREAD_MUTEX_INITIALIZER;
+
+static size_t write_callback(void *contents, size_t size, size_t nmemb,
+                             void *ud) /* {{{ */
+{
+  size_t realsize = size * nmemb;
+  blob_t *blob = ud;
+
+  if ((0x7FFFFFF0 < blob->size) || (0x7FFFFFF0 - blob->size < realsize)) {
+    ERROR("utils_gce: write_callback: integer overflow");
+    return 0;
+  }
+
+  blob->data = realloc(blob->data, blob->size + realsize + 1);
+  if (blob->data == NULL) {
+    /* out of memory! */
+    ERROR(
+        "utils_gce: write_callback: not enough memory (realloc returned NULL)");
+    return 0;
+  }
+
+  memcpy(blob->data + blob->size, contents, realsize);
+  blob->size += realsize;
+  blob->data[blob->size] = 0;
+
+  return realsize;
+} /* }}} size_t write_callback */
+
+/* read_url will issue a GET request for the given URL, setting the magic GCE
+ * metadata header in the process. On success, the response body is returned
+ * and it's the caller's responsibility to free it. On failure, an error is
+ * logged and NULL is returned. */
+static char *read_url(char const *url) /* {{{ */
+{
+  CURL *curl = curl_easy_init();
+  if (!curl) {
+    ERROR("utils_gce: curl_easy_init failed.");
+    return NULL;
+  }
+
+  struct curl_slist *headers = curl_slist_append(NULL, GCE_METADATA_HEADER);
+
+  char curl_errbuf[CURL_ERROR_SIZE];
+  blob_t blob = {0};
+  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
+  curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
+  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
+  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &blob);
+  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+  curl_easy_setopt(curl, CURLOPT_URL, url);
+
+  int status = curl_easy_perform(curl);
+  if (status != CURLE_OK) {
+    ERROR("utils_gce: fetching %s failed: %s", url, curl_errbuf);
+    sfree(blob.data);
+    curl_easy_cleanup(curl);
+    curl_slist_free_all(headers);
+    return NULL;
+  }
+
+  long http_code = 0;
+  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
+  if ((http_code < 200) || (http_code >= 300)) {
+    ERROR("write_gcm plugin: fetching %s failed: HTTP error %ld", url,
+          http_code);
+    sfree(blob.data);
+    curl_easy_cleanup(curl);
+    curl_slist_free_all(headers);
+    return NULL;
+  }
+
+  curl_easy_cleanup(curl);
+  curl_slist_free_all(headers);
+  return blob.data;
+} /* }}} char *read_url */
+
+_Bool gce_check(void) /* {{{ */
+{
+  if (on_gce != -1)
+    return on_gce == 1;
+
+  DEBUG("utils_gce: Checking whether I'm running on GCE ...");
+
+  CURL *curl = curl_easy_init();
+  if (!curl) {
+    ERROR("utils_gce: curl_easy_init failed.");
+    return 0;
+  }
+
+  struct curl_slist *headers = curl_slist_append(NULL, GCE_METADATA_HEADER);
+
+  char curl_errbuf[CURL_ERROR_SIZE];
+  blob_t blob = {NULL, 0};
+  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
+  curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
+  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+  curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, write_callback);
+  curl_easy_setopt(curl, CURLOPT_WRITEHEADER, &blob);
+  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+  curl_easy_setopt(curl, CURLOPT_URL, GCP_METADATA_PREFIX "/");
+
+  int status = curl_easy_perform(curl);
+  if ((status != CURLE_OK) || (blob.data == NULL) ||
+      (strstr(blob.data, "Metadata-Flavor: Google") == NULL)) {
+    DEBUG("utils_gce: ... no (%s)",
+          (status != CURLE_OK)
+              ? "curl_easy_perform failed"
+              : (blob.data == NULL) ? "blob.data == NULL"
+                                    : "Metadata-Flavor header not found");
+    sfree(blob.data);
+    curl_easy_cleanup(curl);
+    curl_slist_free_all(headers);
+    on_gce = 0;
+    return 0;
+  }
+  sfree(blob.data);
+
+  long http_code = 0;
+  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
+  if ((http_code < 200) || (http_code >= 300)) {
+    DEBUG("utils_gce: ... no (HTTP status %ld)", http_code);
+    curl_easy_cleanup(curl);
+    curl_slist_free_all(headers);
+    on_gce = 0;
+    return 0;
+  }
+
+  DEBUG("utils_gce: ... yes");
+  curl_easy_cleanup(curl);
+  curl_slist_free_all(headers);
+  on_gce = 1;
+  return 1;
+} /* }}} _Bool gce_check */
+
+char *gce_project_id(void) /* {{{ */
+{
+  return read_url(GCE_PROJECT_ID_URL);
+} /* }}} char *gce_project_id */
+
+char *gce_instance_id(void) /* {{{ */
+{
+  return read_url(GCE_INSTANCE_ID_URL);
+} /* }}} char *gce_instance_id */
+
+char *gce_zone(void) /* {{{ */
+{
+  return read_url(GCE_ZONE_URL);
+} /* }}} char *gce_instance_id */
+
+char *gce_scope(char const *email) /* {{{ */
+{
+  char url[1024];
+
+  snprintf(url, sizeof(url), GCE_SCOPE_URL_FORMAT,
+           (email != NULL) ? email : GCE_DEFAULT_SERVICE_ACCOUNT);
+
+  return read_url(url);
+} /* }}} char *gce_scope */
+
+int gce_access_token(char const *email, char *buffer,
+                     size_t buffer_size) /* {{{ */
+{
+  char url[1024];
+  char *json;
+  cdtime_t now = cdtime();
+
+  pthread_mutex_lock(&token_lock);
+
+  if (email == NULL)
+    email = GCE_DEFAULT_SERVICE_ACCOUNT;
+
+  if ((token_email != NULL) && (strcmp(email, token_email) == 0) &&
+      (token_valid_until > now)) {
+    sstrncpy(buffer, token, buffer_size);
+    pthread_mutex_unlock(&token_lock);
+    return 0;
+  }
+
+  snprintf(url, sizeof(url), GCE_TOKEN_URL_FORMAT, email);
+  json = read_url(url);
+  if (json == NULL) {
+    pthread_mutex_unlock(&token_lock);
+    return -1;
+  }
+
+  char tmp[256];
+  cdtime_t expires_in = 0;
+  int status = oauth_parse_json_token(json, tmp, sizeof(tmp), &expires_in);
+  sfree(json);
+  if (status != 0) {
+    pthread_mutex_unlock(&token_lock);
+    return status;
+  }
+
+  sfree(token);
+  token = strdup(tmp);
+
+  sfree(token_email);
+  token_email = strdup(email);
+
+  /* let tokens expire a bit early */
+  expires_in = (expires_in * 95) / 100;
+  token_valid_until = now + expires_in;
+
+  sstrncpy(buffer, token, buffer_size);
+  pthread_mutex_unlock(&token_lock);
+  return 0;
+} /* }}} char *gce_token */
diff --git a/src/utils_gce.h b/src/utils_gce.h
new file mode 100644 (file)
index 0000000..2ee3f6e
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * collectd - src/utils_gce.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_GCE_H
+#define UTILS_GCE_H 1
+
+/* gce_check returns 1 when running on Google Compute Engine (GCE) and 0
+ * otherwise. */
+_Bool gce_check(void);
+
+/* gce_project_id returns the project ID of the instance, as configured when
+ * creating the project.
+ * For example "example-project-a". */
+char *gce_project_id(void);
+
+/* gce_instance_id returns the unique ID of the GCE instance. */
+char *gce_instance_id(void);
+
+/* gce_zone returns the zone in which the GCE instance runs. */
+char *gce_zone(void);
+
+/* gce_scope returns the list of scopes for the given service account (or the
+ * default service account when NULL is passed). */
+char *gce_scope(char const *email);
+
+/* gce_access_token acquires an OAuth access token for the given service account
+ * (or
+ * the default service account when NULL is passed) and stores it in buffer.
+ * Access tokens are automatically cached and renewed when they expire. Returns
+ * zero on success, non-zero otherwise. */
+int gce_access_token(char const *email, char *buffer, size_t buffer_size);
+
+#endif
diff --git a/src/utils_oauth.c b/src/utils_oauth.c
new file mode 100644 (file)
index 0000000..35533be
--- /dev/null
@@ -0,0 +1,636 @@
+/**
+ * collectd - src/utils_oauth.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 "plugin.h"
+#include "utils_oauth.h"
+
+#include <curl/curl.h>
+
+#include <yajl/yajl_tree.h>
+#include <yajl/yajl_version.h>
+
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/pem.h>
+#include <openssl/pkcs12.h>
+#include <openssl/sha.h>
+
+/*
+ * Private variables
+ */
+#define GOOGLE_TOKEN_URL "https://accounts.google.com/o/oauth2/token"
+
+/* Max send buffer size, since there will be only one writer thread and
+ * monitoring api supports up to 100K bytes in one request, 64K is reasonable
+ */
+#define MAX_BUFFER_SIZE 65536
+#define MAX_ENCODE_SIZE 2048
+
+struct oauth_s {
+  char *url;
+  char *iss;
+  char *aud;
+  char *scope;
+
+  EVP_PKEY *key;
+
+  char *token;
+  cdtime_t valid_until;
+};
+
+struct memory_s {
+  char *memory;
+  size_t size;
+};
+typedef struct memory_s memory_t;
+
+#define OAUTH_GRANT_TYPE "urn:ietf:params:oauth:grant-type:jwt-bearer"
+#define OAUTH_EXPIRATION_TIME TIME_T_TO_CDTIME_T(3600)
+#define OAUTH_HEADER "{\"alg\":\"RS256\",\"typ\":\"JWT\"}"
+
+static const char OAUTH_CLAIM_FORMAT[] = "{"
+                                         "\"iss\":\"%s\","
+                                         "\"scope\":\"%s\","
+                                         "\"aud\":\"%s\","
+                                         "\"exp\":%lu,"
+                                         "\"iat\":%lu"
+                                         "}";
+
+static size_t write_memory(void *contents, size_t size, size_t nmemb, /* {{{ */
+                           void *userp) {
+  size_t realsize = size * nmemb;
+  memory_t *mem = (memory_t *)userp;
+  char *tmp;
+
+  if (0x7FFFFFF0 < mem->size || 0x7FFFFFF0 - mem->size < realsize) {
+    ERROR("integer overflow");
+    return 0;
+  }
+
+  tmp = (char *)realloc((void *)mem->memory, mem->size + realsize + 1);
+  if (tmp == NULL) {
+    /* out of memory! */
+    ERROR("write_memory: not enough memory (realloc returned NULL)");
+    return 0;
+  }
+  mem->memory = tmp;
+
+  memcpy(&(mem->memory[mem->size]), contents, realsize);
+  mem->size += realsize;
+  mem->memory[mem->size] = 0;
+
+  return realsize;
+} /* }}} size_t write_memory */
+
+/* Base64-encodes "s" and stores the result in buffer.
+ * Returns zero on success, non-zero otherwise. */
+static int base64_encode_n(char const *s, size_t s_size, /* {{{ */
+                           char *buffer, size_t buffer_size) {
+  BIO *b64;
+  BUF_MEM *bptr;
+  int status;
+  size_t i;
+
+  /* Set up the memory-base64 chain */
+  b64 = BIO_new(BIO_f_base64());
+  BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
+  b64 = BIO_push(b64, BIO_new(BIO_s_mem()));
+
+  /* Write data to the chain */
+  BIO_write(b64, (void const *)s, s_size);
+  status = BIO_flush(b64);
+  if (status != 1) {
+    ERROR("utils_oauth: base64_encode: BIO_flush() failed.");
+    BIO_free_all(b64);
+    return -1;
+  }
+
+  /* Never fails */
+  BIO_get_mem_ptr(b64, &bptr);
+
+  if (buffer_size <= bptr->length) {
+    ERROR("utils_oauth: base64_encode: Buffer too small.");
+    BIO_free_all(b64);
+    return -1;
+  }
+
+  /* Copy data to buffer. */
+  memcpy(buffer, bptr->data, bptr->length);
+  buffer[bptr->length] = 0;
+
+  /* replace + with -, / with _ and remove padding = at the end */
+  for (i = 0; i < bptr->length; i++) {
+    if (buffer[i] == '+') {
+      buffer[i] = '-';
+    } else if (buffer[i] == '/') {
+      buffer[i] = '_';
+    } else if (buffer[i] == '=') {
+      buffer[i] = 0;
+    }
+  }
+
+  BIO_free_all(b64);
+  return 0;
+} /* }}} int base64_encode_n */
+
+/* Base64-encodes "s" and stores the result in buffer.
+ * Returns zero on success, non-zero otherwise. */
+static int base64_encode(char const *s, /* {{{ */
+                         char *buffer, size_t buffer_size) {
+  return base64_encode_n(s, strlen(s), buffer, buffer_size);
+} /* }}} int base64_encode */
+
+/* get_header returns the base64 encoded OAuth header. */
+static int get_header(char *buffer, size_t buffer_size) /* {{{ */
+{
+  char header[] = OAUTH_HEADER;
+
+  return base64_encode(header, buffer, buffer_size);
+} /* }}} int get_header */
+
+/* get_claim constructs an OAuth claim and returns it as base64 encoded string.
+ */
+static int get_claim(oauth_t *auth, char *buffer, size_t buffer_size) /* {{{ */
+{
+  char claim[buffer_size];
+  cdtime_t exp;
+  cdtime_t iat;
+  int status;
+
+  iat = cdtime();
+  exp = iat + OAUTH_EXPIRATION_TIME;
+
+  /* create the claim set */
+  status =
+      snprintf(claim, sizeof(claim), OAUTH_CLAIM_FORMAT, auth->iss, auth->scope,
+               auth->aud, (unsigned long)CDTIME_T_TO_TIME_T(exp),
+               (unsigned long)CDTIME_T_TO_TIME_T(iat));
+  if (status < 1)
+    return -1;
+  else if ((size_t)status >= sizeof(claim))
+    return ENOMEM;
+
+  DEBUG("utils_oauth: get_claim() = %s", claim);
+
+  return base64_encode(claim, buffer, buffer_size);
+} /* }}} int get_claim */
+
+/* get_signature signs header and claim with pkey and returns the signature in
+ * buffer. */
+static int get_signature(char *buffer, size_t buffer_size, /* {{{ */
+                         char const *header, char const *claim,
+                         EVP_PKEY *pkey) {
+  char payload[buffer_size];
+  size_t payload_len;
+  char signature[buffer_size];
+  unsigned int signature_size;
+  int status;
+
+  /* Make the string to sign */
+  payload_len = snprintf(payload, sizeof(payload), "%s.%s", header, claim);
+  if (payload_len < 1) {
+    return -1;
+  } else if (payload_len >= sizeof(payload)) {
+    return ENOMEM;
+  }
+
+  /* Create the signature */
+  signature_size = EVP_PKEY_size(pkey);
+  if (signature_size > sizeof(signature)) {
+    ERROR("utils_oauth: Signature is too large (%u bytes).", signature_size);
+    return -1;
+  }
+
+  EVP_MD_CTX *ctx = EVP_MD_CTX_new();
+
+  /* EVP_SignInit(3SSL) claims this is a void function, but in fact it returns
+   * an int. We're not going to rely on this, though. */
+  EVP_SignInit(ctx, EVP_sha256());
+
+  status = EVP_SignUpdate(ctx, payload, payload_len);
+  if (status != 1) {
+    char errbuf[1024];
+    ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
+    ERROR("utils_oauth: EVP_SignUpdate failed: %s", errbuf);
+
+    EVP_MD_CTX_free(ctx);
+    return -1;
+  }
+
+  status =
+      EVP_SignFinal(ctx, (unsigned char *)signature, &signature_size, pkey);
+  if (status != 1) {
+    char errbuf[1024];
+    ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
+    ERROR("utils_oauth: EVP_SignFinal failed: %s", errbuf);
+
+    EVP_MD_CTX_free(ctx);
+    return -1;
+  }
+
+  EVP_MD_CTX_free(ctx);
+
+  return base64_encode_n(signature, (size_t)signature_size, buffer,
+                         buffer_size);
+} /* }}} int get_signature */
+
+static int get_assertion(oauth_t *auth, char *buffer,
+                         size_t buffer_size) /* {{{ */
+{
+  char header[buffer_size];
+  char claim[buffer_size];
+  char signature[buffer_size];
+  int status;
+
+  status = get_header(header, sizeof(header));
+  if (status != 0)
+    return -1;
+
+  status = get_claim(auth, claim, sizeof(claim));
+  if (status != 0)
+    return -1;
+
+  status =
+      get_signature(signature, sizeof(signature), header, claim, auth->key);
+  if (status != 0)
+    return -1;
+
+  status = snprintf(buffer, buffer_size, "%s.%s.%s", header, claim, signature);
+  if (status < 1)
+    return -1;
+  else if (status >= buffer_size)
+    return ENOMEM;
+
+  return 0;
+} /* }}} int get_assertion */
+
+int oauth_parse_json_token(char const *json, /* {{{ */
+                           char *out_access_token, size_t access_token_size,
+                           cdtime_t *expires_in) {
+  time_t expire_in_seconds = 0;
+  yajl_val root;
+  yajl_val token_val;
+  yajl_val expire_val;
+  char errbuf[1024];
+  const char *token_path[] = {"access_token", NULL};
+  const char *expire_path[] = {"expires_in", NULL};
+
+  root = yajl_tree_parse(json, errbuf, sizeof(errbuf));
+  if (root == NULL) {
+    ERROR("utils_oauth: oauth_parse_json_token: parse error %s", errbuf);
+    return -1;
+  }
+
+  token_val = yajl_tree_get(root, token_path, yajl_t_string);
+  if (token_val == NULL) {
+    ERROR("utils_oauth: oauth_parse_json_token: access token field not found");
+    yajl_tree_free(root);
+    return -1;
+  }
+  sstrncpy(out_access_token, YAJL_GET_STRING(token_val), access_token_size);
+
+  expire_val = yajl_tree_get(root, expire_path, yajl_t_number);
+  if (expire_val == NULL) {
+    ERROR("utils_oauth: oauth_parse_json_token: expire field found");
+    yajl_tree_free(root);
+    return -1;
+  }
+  expire_in_seconds = (time_t)YAJL_GET_INTEGER(expire_val);
+  DEBUG("oauth_parse_json_token: expires_in %lu",
+        (unsigned long)expire_in_seconds);
+
+  *expires_in = TIME_T_TO_CDTIME_T(expire_in_seconds);
+  yajl_tree_free(root);
+  return 0;
+} /* }}} int oauth_parse_json_token */
+
+static int new_token(oauth_t *auth) /* {{{ */
+{
+  CURL *curl;
+  char assertion[1024];
+  char post_data[1024];
+  memory_t data;
+  char access_token[256];
+  cdtime_t expires_in;
+  cdtime_t now;
+  char curl_errbuf[CURL_ERROR_SIZE];
+  int status = 0;
+
+  data.size = 0;
+  data.memory = NULL;
+
+  now = cdtime();
+
+  status = get_assertion(auth, assertion, sizeof(assertion));
+  if (status != 0) {
+    ERROR("utils_oauth: Failed to get token using service account %s.",
+          auth->iss);
+    return -1;
+  }
+
+  snprintf(post_data, sizeof(post_data), "grant_type=%s&assertion=%s",
+           OAUTH_GRANT_TYPE, assertion);
+
+  curl = curl_easy_init();
+  if (curl == NULL) {
+    ERROR("utils_oauth: curl_easy_init failed.");
+    return -1;
+  }
+
+  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
+  curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
+  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_memory);
+  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
+  curl_easy_setopt(curl, CURLOPT_POST, 1L);
+  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
+  curl_easy_setopt(curl, CURLOPT_URL, auth->url);
+
+  status = curl_easy_perform(curl);
+  if (status != CURLE_OK) {
+    ERROR("utils_oauth: curl_easy_perform failed with status %i: %s", status,
+          curl_errbuf);
+
+    sfree(data.memory);
+    curl_easy_cleanup(curl);
+
+    return -1;
+  } else {
+    long http_code = 0;
+
+    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
+    if ((http_code < 200) || (http_code >= 300)) {
+      ERROR("utils_oauth: POST request to %s failed: HTTP error %ld", auth->url,
+            http_code);
+      if (data.memory != NULL)
+        INFO("utils_oauth: Server replied: %s", data.memory);
+
+      sfree(data.memory);
+      curl_easy_cleanup(curl);
+
+      return -1;
+    }
+  }
+
+  status = oauth_parse_json_token(data.memory, access_token,
+                                  sizeof(access_token), &expires_in);
+  if (status != 0) {
+    sfree(data.memory);
+    curl_easy_cleanup(curl);
+
+    return -1;
+  }
+
+  sfree(auth->token);
+  auth->token = strdup(access_token);
+  if (auth->token == NULL) {
+    ERROR("utils_oauth: strdup failed");
+    auth->valid_until = 0;
+
+    sfree(data.memory);
+    curl_easy_cleanup(curl);
+    return -1;
+  }
+
+  INFO("utils_oauth: OAuth2 access token is valid for %.3fs",
+       CDTIME_T_TO_DOUBLE(expires_in));
+  auth->valid_until = now + expires_in;
+
+  sfree(data.memory);
+  curl_easy_cleanup(curl);
+
+  return 0;
+} /* }}} int new_token */
+
+static int renew_token(oauth_t *auth) /* {{{ */
+{
+  /* TODO(octo): Make sure that we get a new token 60 seconds or so before the
+   * old one expires. */
+  if (auth->valid_until > cdtime())
+    return 0;
+
+  return new_token(auth);
+} /* }}} int renew_token */
+
+static oauth_t *oauth_create(char const *url, char const *iss,
+                             char const *scope, char const *aud,
+                             EVP_PKEY *key) /* {{{ */
+{
+  oauth_t *auth;
+
+  if ((url == NULL) || (iss == NULL) || (scope == NULL) || (aud == NULL) ||
+      (key == NULL))
+    return NULL;
+
+  auth = malloc(sizeof(*auth));
+  if (auth == NULL)
+    return NULL;
+  memset(auth, 0, sizeof(*auth));
+
+  auth->url = strdup(url);
+  auth->iss = strdup(iss);
+  auth->scope = strdup(scope);
+  auth->aud = strdup(aud);
+
+  if ((auth->url == NULL) || (auth->iss == NULL) || (auth->scope == NULL) ||
+      (auth->aud == NULL)) {
+    oauth_destroy(auth);
+    return NULL;
+  }
+
+  auth->key = key;
+
+  return auth;
+} /* }}} oauth_t *oauth_create */
+
+/*
+ * Public
+ */
+oauth_google_t oauth_create_google_json(char const *buffer, char const *scope) {
+  char errbuf[1024];
+  yajl_val root = yajl_tree_parse(buffer, errbuf, sizeof(errbuf));
+  if (root == NULL) {
+    ERROR("utils_oauth: oauth_create_google_json: parse error %s", errbuf);
+    return (oauth_google_t){NULL};
+  }
+
+  yajl_val field_project =
+      yajl_tree_get(root, (char const *[]){"project_id", NULL}, yajl_t_string);
+  if (field_project == NULL) {
+    ERROR("utils_oauth: oauth_create_google_json: project_id field not found");
+    yajl_tree_free(root);
+    return (oauth_google_t){NULL};
+  }
+  char const *project_id = YAJL_GET_STRING(field_project);
+
+  yajl_val field_iss = yajl_tree_get(
+      root, (char const *[]){"client_email", NULL}, yajl_t_string);
+  if (field_iss == NULL) {
+    ERROR(
+        "utils_oauth: oauth_create_google_json: client_email field not found");
+    yajl_tree_free(root);
+    return (oauth_google_t){NULL};
+  }
+
+  yajl_val field_token_uri =
+      yajl_tree_get(root, (char const *[]){"token_uri", NULL}, yajl_t_string);
+  char const *token_uri = (field_token_uri != NULL)
+                              ? YAJL_GET_STRING(field_token_uri)
+                              : GOOGLE_TOKEN_URL;
+
+  yajl_val field_priv_key =
+      yajl_tree_get(root, (char const *[]){"private_key", NULL}, yajl_t_string);
+  if (field_priv_key == NULL) {
+    ERROR("utils_oauth: oauth_create_google_json: private_key field not found");
+    yajl_tree_free(root);
+    return (oauth_google_t){NULL};
+  }
+
+  BIO *bp = BIO_new_mem_buf(YAJL_GET_STRING(field_priv_key), -1);
+  EVP_PKEY *pkey = PEM_read_bio_PrivateKey(bp, NULL, NULL, NULL);
+  if (pkey == NULL) {
+    char errbuf[1024];
+    ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf));
+    ERROR(
+        "utils_oauth: oauth_create_google_json: parsing private key failed: %s",
+        errbuf);
+    BIO_free(bp);
+    yajl_tree_free(root);
+    return (oauth_google_t){NULL};
+  }
+
+  BIO_free(bp);
+
+  oauth_t *oauth = oauth_create(token_uri, YAJL_GET_STRING(field_iss), scope,
+                                token_uri, pkey);
+  if (oauth == NULL) {
+    yajl_tree_free(root);
+    return (oauth_google_t){NULL};
+  }
+
+  oauth_google_t ret = {
+      .project_id = strdup(project_id), .oauth = oauth,
+  };
+
+  yajl_tree_free(root);
+  return ret;
+} /* oauth_google_t oauth_create_google_json */
+
+oauth_google_t oauth_create_google_file(char const *path,
+                                        char const *scope) { /* {{{ */
+  int fd = open(path, O_RDONLY);
+  if (fd == -1)
+    return (oauth_google_t){NULL};
+
+  struct stat st = {0};
+  if (fstat(fd, &st) != 0) {
+    close(fd);
+    return (oauth_google_t){NULL};
+  }
+
+  size_t buf_size = (size_t)st.st_size;
+  char *buf = calloc(1, buf_size + 1);
+  if (buf == NULL) {
+    close(fd);
+    return (oauth_google_t){NULL};
+  }
+
+  if (sread(fd, buf, buf_size) != 0) {
+    free(buf);
+    close(fd);
+    return (oauth_google_t){NULL};
+  }
+  close(fd);
+  buf[buf_size] = 0;
+
+  oauth_google_t ret = oauth_create_google_json(buf, scope);
+
+  free(buf);
+  return ret;
+} /* }}} oauth_google_t oauth_create_google_file */
+
+/* oauth_create_google_default checks for JSON credentials in well-known
+ * positions, similar to gcloud and other tools. */
+oauth_google_t oauth_create_google_default(char const *scope) {
+  char const *app_creds;
+  if ((app_creds = getenv("GOOGLE_APPLICATION_CREDENTIALS")) != NULL) {
+    oauth_google_t ret = oauth_create_google_file(app_creds, scope);
+    if (ret.oauth == NULL) {
+      ERROR("The environment variable GOOGLE_APPLICATION_CREDENTIALS is set to "
+            "\"%s\" but that file could not be read.",
+            app_creds);
+    } else {
+      return ret;
+    }
+  }
+
+  char const *home;
+  if ((home = getenv("HOME")) != NULL) {
+    char path[PATH_MAX];
+    snprintf(path, sizeof(path),
+             "%s/.config/gcloud/application_default_credentials.json", home);
+
+    oauth_google_t ret = oauth_create_google_file(path, scope);
+    if (ret.oauth != NULL) {
+      return ret;
+    }
+  }
+
+  return (oauth_google_t){NULL};
+} /* }}} oauth_google_t oauth_create_google_default */
+
+void oauth_destroy(oauth_t *auth) /* {{{ */
+{
+  if (auth == NULL)
+    return;
+
+  sfree(auth->url);
+  sfree(auth->iss);
+  sfree(auth->scope);
+  sfree(auth->aud);
+
+  if (auth->key != NULL) {
+    EVP_PKEY_free(auth->key);
+    auth->key = NULL;
+  }
+
+  sfree(auth);
+} /* }}} void oauth_destroy */
+
+int oauth_access_token(oauth_t *auth, char *buffer,
+                       size_t buffer_size) /* {{{ */
+{
+  int status;
+
+  if (auth == NULL)
+    return EINVAL;
+
+  status = renew_token(auth);
+  if (status != 0)
+    return status;
+  assert(auth->token != NULL);
+
+  sstrncpy(buffer, auth->token, buffer_size);
+  return 0;
+} /* }}} int oauth_access_token */
diff --git a/src/utils_oauth.h b/src/utils_oauth.h
new file mode 100644 (file)
index 0000000..b93c87b
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * collectd - src/utils_oauth.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_OAUTH_H
+#define UTILS_OAUTH_H
+
+#include "collectd.h"
+#include "utils_time.h"
+
+#ifndef GOOGLE_OAUTH_URL
+#define GOOGLE_OAUTH_URL "https://www.googleapis.com/oauth2/v3/token"
+#endif
+
+struct oauth_s;
+typedef struct oauth_s oauth_t;
+
+int oauth_parse_json_token(char const *json, char *out_access_token,
+                           size_t access_token_size, cdtime_t *expires_in);
+
+typedef struct {
+  char *project_id;
+  oauth_t *oauth;
+} oauth_google_t;
+
+/* oauth_create_google_json creates an OAuth object from JSON encoded
+ * credentials. */
+oauth_google_t oauth_create_google_json(char const *json, char const *scope);
+
+/* oauth_create_google_file reads path, which contains JSON encoded service
+ * account credentials, and returns an OAuth object. */
+oauth_google_t oauth_create_google_file(char const *path, char const *scope);
+
+/* oauth_create_google_default looks for service account credentials in a couple
+ * of well-known places and returns an OAuth object if found. The well known
+ * locations are:
+ *
+ *   - ${GOOGLE_APPLICATION_CREDENTIALS}
+ *   - ${HOME}/.config/gcloud/application_default_credentials.json
+ */
+oauth_google_t oauth_create_google_default(char const *scope);
+
+/* oauth_destroy frees all resources associated with an OAuth object. */
+void oauth_destroy(oauth_t *auth);
+
+int oauth_access_token(oauth_t *auth, char *buffer, size_t buffer_size);
+
+#endif
diff --git a/src/utils_oauth_test.c b/src/utils_oauth_test.c
new file mode 100644 (file)
index 0000000..791564f
--- /dev/null
@@ -0,0 +1,149 @@
+/**
+ * collectd - src/tests/utils_oauth_test.c
+ * Copyright (C) 2015  Google Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * Authors:
+ *   Florian Forster <octo at google.com>
+ **/
+
+#include "testing.h"
+#include "utils_oauth.h"
+
+struct {
+  char *json;
+  int status;
+  char *access_token;
+  cdtime_t expires_in;
+} cases[] = {
+    {
+        "{\"access_token\":\"MaeC6kaePhie1ree\",\"expires_in\":3600}",
+        /* status = */ 0, "MaeC6kaePhie1ree", TIME_T_TO_CDTIME_T_STATIC(3600),
+    },
+    {
+        "{\"token_type\":\"Bearer\",\"expires_in\":1800,\"access_token\":"
+        "\"aeThiebee2gushuY\"}",
+        /* status = */ 0, "aeThiebee2gushuY", TIME_T_TO_CDTIME_T_STATIC(1800),
+    },
+    {
+        "{\"ignored_key\":\"uaph5aewaeghi1Ge\",\"expires_in\":3600}",
+        /* status = */ -1, NULL, 0,
+    },
+    {
+        /* expires_in missing */
+        "{\"access_token\":\"shaephohbie9Ahch\"}",
+        /* status = */ -1, NULL, 0,
+    },
+};
+
+DEF_TEST(simple) /* {{{ */
+{
+  size_t i;
+  _Bool success = 1;
+
+  for (i = 0; i < (sizeof(cases) / sizeof(cases[0])); i++) {
+    char buffer[1024];
+    cdtime_t expires_in;
+
+    EXPECT_EQ_INT(cases[i].status,
+                  oauth_parse_json_token(cases[i].json, buffer, sizeof(buffer),
+                                         &expires_in));
+    if (cases[i].status != 0)
+      continue;
+
+    EXPECT_EQ_STR(cases[i].access_token, buffer);
+    EXPECT_EQ_UINT64(cases[i].expires_in, expires_in);
+  }
+
+  return success ? 0 : -1;
+} /* }}} simple */
+
+DEF_TEST(oauth_create_google_json) {
+  char const *in =
+      "{\"type\": \"service_account\","
+      "\"project_id\":\"collectd.org:unit-test\","
+      "\"private_key_id\": \"ed7b4eb6c1b61a7bedab5bcafff374f7fc820698\","
+      "\"private_key\":\"-----BEGIN PRIVATE KEY-----\\n"
+      "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDNvS71Lr2WIEqx\\n"
+      "U766iJGORVVib0FnHhOf/0FEI4Hw+tF11vP3LZj0AyQFIi/h2l2EDXOr43C6Gt+K\\n"
+      "0stsyaWvRNzeQa+dUFY5A/ZEtdvYVPq7KudML5Hs9DNmWFlM/iIfQyIUJ+vHv7fe\\n"
+      "pJGgu4ZgSkNWehmWj3qiRzIvYxKvDIQizqPZNlTh+33KQcT2x+ErkuB3snQu8hSK\\n"
+      "HAg2sCvORqKGOvN9F4bAqXt5T0NVjGy4YXeuif1p/Np/GH6Ys1p+etgGwvIimXIv\\n"
+      "jFL9K/ZtrTOcFdy4R5bwrj2piCZa2T5H6fupVp2tVgIuS53r2fEaBMLD97oAvwZ3\\n"
+      "9XPxG1NLAgMBAAECggEACgHroKcrN1FkdgyzSIKFG1evCBCOV17kqHyI5wYXzNTT\\n"
+      "zyNrZDjBFGQkt+U0/AucTznnnahSCZNuD+QiBgLRqYgJevwp99Z6YzVDS438Xsuq\\n"
+      "Ezmf3O+sGEu78Pys11cTP38LT3yuS4iSqo9Jus5JrTG05dDJoYO4J4rxW3xlDRj8\\n"
+      "lQUimXI+S9skaSusf0oErDrjuQG9dxmhnGcSEX+rIe9G0UygTNuI0KKGJ8jmnPz5\\n"
+      "OS+sM8qrKcnjrvENFWKLb11HlliHkh6dILoO5rvf5DR+XGKM7BFAsdWg6oI7SFGh\\n"
+      "S6zGZ0jUR7QAugrjbTlDOCnAuZ+Mbc/4yHZ3u5PlcQKBgQDuvH1ds1YmmbOllOK5\\n"
+      "JtkdjCUUyH1bgkMrmcg/KkRARPRHQvfAioZsC6d0fa6jq0kTW/3Zu14IsVXgM8xK\\n"
+      "fuNSp8LdY+NCtJnfvdLaChgAwZaQLX4qgV0qYw8iLv5ifa4ZY0qaZioJCzkv57y1\\n"
+      "KkavYvITboO7aUSa441Zko9c+wKBgQDcndg0QpWH6JMz/FkCf/KDyW/cUODfKXhP\\n"
+      "5p9eTcVlfDL2sAb2RzVhvKZcuWXVwnfaDP0oBj2/SBLGx0idUb+VHdM/IGiLroyK\\n"
+      "pAHpNM//dowiGL1qPPOLXrzF/vn+w4t2Dqggfcqu52SzRiyaxUtSMnNyyyU19cO+\\n"
+      "pb7wAS5x8QKBgCW7WL0UeQtEw6Xp8CN/RlVrLvkn7tglsGQVvBZvobXesBULOokN\\n"
+      "28z70o2Qx6dKjRQoN+jPuj75eC8lQKaNg3Qu25eOD/8c+CzqnYakjcKg1iEXb5dc\\n"
+      "NtNaMKwgbUg3wOp2TPY2K3KeeX1ezO59LgrOQqBbmSpnqtYoHNEJXus9AoGAWl/y\\n"
+      "9J2eIdm9i5tBX0vIrgHz5/3d0K1tUtX3zSrwxT0Wp4W+pF7RWGNuhyePtvx+Gn4d\\n"
+      "qqq72sMMpg93CLM3Vz+rjP2atjXf7t92xPDUkCMhDsqxtXaYkixSCo4EHUA/vjIM\\n"
+      "35qIUBQMZYBGv3Q5AcgXERx09uDhuhSt3iWtwBECgYAHFnCh8fKsJbQrVN10tU/h\\n"
+      "ofVx0KZkUpBz8eNQPuxt4aY+LyWsKVKtnduw2WdumuOY66cUN1lsi8Bz/cq1dhPt\\n"
+      "Oc2S7pqjbu2Q1Oqx+/yr6jqsvKaSxHmcpbWQBsGn6UaWZgYZcAtQBcqDAp7pylwj\\n"
+      "tejRh0NB8d81H5Dli1Qfzw==\\n"
+      "-----END PRIVATE KEY-----\\n\","
+      "\"client_email\":\"example-sacct@unit-test.iam.gserviceaccount.com\", "
+      "\"client_id\": \"109958449193027604084\","
+      "\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\","
+      "\"token_uri\":\"https://accounts.google.com/o/oauth2/token\","
+      "\"auth_provider_x509_cert_url\":"
+      "\"https://www.googleapis.com/oauth2/v1/certs\","
+      "\"client_x509_cert_url\":\"https://www.googleapis.com/robot/v1/"
+      "metadata/x509/example-sacct%40ssc-serv-dev.iam.gserviceaccount.com\"}";
+
+  oauth_google_t ret =
+      oauth_create_google_json(in, "https://collectd.org/example.scope");
+
+  EXPECT_EQ_STR("collectd.org:unit-test", ret.project_id);
+
+  CHECK_NOT_NULL(ret.oauth);
+  struct {
+    char *url;
+    char *iss;
+    char *aud;
+    char *scope;
+  } *obj = (void *)ret.oauth;
+
+  EXPECT_EQ_STR("https://accounts.google.com/o/oauth2/token", obj->url);
+  EXPECT_EQ_STR("example-sacct@unit-test.iam.gserviceaccount.com", obj->iss);
+  EXPECT_EQ_STR("https://collectd.org/example.scope", obj->scope);
+
+  free(ret.project_id);
+  oauth_destroy(ret.oauth);
+
+  return 0;
+}
+
+int main(int argc, char **argv) /* {{{ */
+{
+  RUN_TEST(simple);
+  RUN_TEST(oauth_create_google_json);
+
+  END_TEST;
+} /* }}} int main */
diff --git a/src/write_stackdriver.c b/src/write_stackdriver.c
new file mode 100644 (file)
index 0000000..9d06c25
--- /dev/null
@@ -0,0 +1,692 @@
+/**
+ * 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>
+#include <yajl/yajl_tree.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 */
+
+typedef struct {
+  int code;
+  char *message;
+} api_error_t;
+
+static api_error_t *parse_api_error(char const *body) {
+  char errbuf[1024];
+  yajl_val root = yajl_tree_parse(body, errbuf, sizeof(errbuf));
+  if (root == NULL) {
+    ERROR("write_stackdriver plugin: yajl_tree_parse failed: %s", errbuf);
+    return NULL;
+  }
+
+  api_error_t *err = calloc(1, sizeof(*err));
+  if (err == NULL) {
+    ERROR("write_stackdriver plugin: calloc failed");
+    yajl_tree_free(root);
+    return NULL;
+  }
+
+  yajl_val code = yajl_tree_get(root, (char const *[]){"error", "code", NULL},
+                                yajl_t_number);
+  if (YAJL_IS_INTEGER(code)) {
+    err->code = YAJL_GET_INTEGER(code);
+  }
+
+  yajl_val message = yajl_tree_get(
+      root, (char const *[]){"error", "message", NULL}, yajl_t_string);
+  if (YAJL_IS_STRING(message)) {
+    char const *m = YAJL_GET_STRING(message);
+    if (m != NULL) {
+      err->message = strdup(m);
+    }
+  }
+
+  return err;
+}
+
+static char *api_error_string(api_error_t *err, char *buffer,
+                              size_t buffer_size) {
+  if (err == NULL) {
+    strncpy(buffer, "Unknown error (API error is NULL)", buffer_size);
+  } else if (err->message == NULL) {
+    snprintf(buffer, buffer_size, "API error %d", err->code);
+  } else {
+    snprintf(buffer, buffer_size, "API error %d: %s", err->code, err->message);
+  }
+
+  return buffer;
+}
+#define API_ERROR_STRING(err) api_error_string(err, (char[1024]){""}, 1024)
+
+// do_post does a HTTP POST request, assuming a JSON payload and using OAuth
+// authentication. Returns -1 on error and the HTTP status code otherwise.
+// ret_content, if not NULL, will contain the server's response.
+// If ret_content is provided and the server responds with a 4xx or 5xx error,
+// an appropriate message will be logged.
+static int do_post(wg_callback_t *cb, char const *url, void const *payload,
+                   wg_memory_t *ret_content) {
+  if (cb->curl == NULL) {
+    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_ERRORBUFFER, cb->curl_errbuf);
+    curl_easy_setopt(cb->curl, CURLOPT_NOSIGNAL, 1L);
+  }
+
+  curl_easy_setopt(cb->curl, CURLOPT_POST, 1L);
+  curl_easy_setopt(cb->curl, CURLOPT_URL, url);
+
+  long timeout_ms = 2 * CDTIME_T_TO_MS(plugin_get_interval());
+  if (timeout_ms < 10000) {
+    timeout_ms = 10000;
+  }
+  curl_easy_setopt(cb->curl, CURLOPT_TIMEOUT_MS, timeout_ms);
+
+  /* header */
+  char *auth_header = wg_get_authorization_header(cb);
+  if (auth_header == NULL) {
+    ERROR("write_stackdriver plugin: getting access token failed with");
+    return -1;
+  }
+
+  struct curl_slist *headers =
+      curl_slist_append(NULL, "Content-Type: application/json");
+  headers = curl_slist_append(headers, auth_header);
+  curl_easy_setopt(cb->curl, CURLOPT_HTTPHEADER, headers);
+
+  curl_easy_setopt(cb->curl, CURLOPT_POSTFIELDS, payload);
+
+  curl_easy_setopt(cb->curl, CURLOPT_WRITEFUNCTION,
+                   ret_content ? wg_write_memory_cb : NULL);
+  curl_easy_setopt(cb->curl, CURLOPT_WRITEDATA, ret_content);
+
+  int status = curl_easy_perform(cb->curl);
+
+  /* clean up that has to happen in any case */
+  curl_slist_free_all(headers);
+  sfree(auth_header);
+  curl_easy_setopt(cb->curl, CURLOPT_HTTPHEADER, NULL);
+  curl_easy_setopt(cb->curl, CURLOPT_WRITEFUNCTION, NULL);
+  curl_easy_setopt(cb->curl, CURLOPT_WRITEDATA, NULL);
+
+  if (status != CURLE_OK) {
+    ERROR("write_stackdriver plugin: POST %s failed: %s", url, cb->curl_errbuf);
+    sfree(ret_content->memory);
+    ret_content->size = 0;
+    return -1;
+  }
+
+  long http_code = 0;
+  curl_easy_getinfo(cb->curl, CURLINFO_RESPONSE_CODE, &http_code);
+
+  if (ret_content != NULL) {
+    if ((status >= 400) && (status < 500)) {
+      ERROR("write_stackdriver plugin: POST %s: %s", url,
+            API_ERROR_STRING(parse_api_error(ret_content->memory)));
+    } else if (status >= 500) {
+      WARNING("write_stackdriver plugin: POST %s: %s", url,
+              ret_content->memory);
+    }
+  }
+
+  return (int)http_code;
+} /* int do_post */
+
+static int wg_call_metricdescriptor_create(wg_callback_t *cb,
+                                           char const *payload) {
+  char url[1024];
+  snprintf(url, sizeof(url), "%s/projects/%s/metricDescriptors", cb->url,
+           cb->project);
+  wg_memory_t response = {0};
+
+  int status = do_post(cb, url, payload, &response);
+  if (status == -1) {
+    ERROR("write_stackdriver plugin: POST %s failed", url);
+    return -1;
+  }
+  sfree(response.memory);
+
+  if (status != 200) {
+    ERROR("write_stackdriver plugin: POST %s: unexpected response code: got "
+          "%d, want 200",
+          url, status);
+    return -1;
+  }
+  return 0;
+} /* int wg_call_metricdescriptor_create */
+
+static int wg_call_timeseries_write(wg_callback_t *cb, char const *payload) {
+  char url[1024];
+  snprintf(url, sizeof(url), "%s/projects/%s/timeSeries", cb->url, cb->project);
+  wg_memory_t response = {0};
+
+  int status = do_post(cb, url, payload, &response);
+  if (status == -1) {
+    ERROR("write_stackdriver plugin: POST %s failed", url);
+    return -1;
+  }
+  sfree(response.memory);
+
+  if (status != 200) {
+    ERROR("write_stackdriver plugin: POST %s: unexpected response code: got "
+          "%d, want 200",
+          url, status);
+    return -1;
+  }
+  return 0;
+} /* int wg_call_timeseries_write */
+
+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_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 (strcasecmp("Label", child->key) == 0) {
+      if ((child->values_num != 2) ||
+          (child->values[0].type != OCONFIG_TYPE_STRING) ||
+          (child->values[1].type != OCONFIG_TYPE_STRING)) {
+        ERROR("write_stackdriver plugin: The \"Label\" option needs exactly "
+              "two string arguments.");
+        continue;
+      }
+
+      sd_resource_add_label(cb->resource, child->values[0].value.string,
+                            child->values[1].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 */