1 /**
2 * collectd - src/write_stackdriver.c
3 * ISC license
4 *
5 * Copyright (C) 2017 Florian Forster
6 *
7 * Permission to use, copy, modify, and/or distribute this software for any
8 * purpose with or without fee is hereby granted, provided that the above
9 * copyright notice and this permission notice appear in all copies.
10 *
11 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18 *
19 * Authors:
20 * Florian Forster <octo at collectd.org>
21 **/
22
23 #include "collectd.h"
24
25 #include "configfile.h"
26 #include "plugin.h"
27 #include "utils/common/common.h"
28 #include "utils/format_stackdriver/format_stackdriver.h"
29 #include "utils/gce/gce.h"
30 #include "utils/oauth/oauth.h"
31
32 #include <curl/curl.h>
33 #include <pthread.h>
34 #include <yajl/yajl_tree.h>
35
36 /*
37 * Private variables
38 */
39 #ifndef GCM_API_URL
40 #define GCM_API_URL "https://monitoring.googleapis.com/v3"
41 #endif
42
43 #ifndef MONITORING_SCOPE
44 #define MONITORING_SCOPE "https://www.googleapis.com/auth/monitoring"
45 #endif
46
47 struct wg_callback_s {
48 /* config */
49 char *email;
50 char *project;
51 char *url;
52 sd_resource_t *resource;
53
54 /* runtime */
55 oauth_t *auth;
56 sd_output_t *formatter;
57 CURL *curl;
58 char curl_errbuf[CURL_ERROR_SIZE];
59 /* used by flush */
60 size_t timeseries_count;
61 cdtime_t send_buffer_init_time;
62
63 pthread_mutex_t lock;
64 };
65 typedef struct wg_callback_s wg_callback_t;
66
67 struct wg_memory_s {
68 char *memory;
69 size_t size;
70 };
71 typedef struct wg_memory_s wg_memory_t;
72
wg_write_memory_cb(void * contents,size_t size,size_t nmemb,void * userp)73 static size_t wg_write_memory_cb(void *contents, size_t size,
74 size_t nmemb, /* {{{ */
75 void *userp) {
76 size_t realsize = size * nmemb;
77 wg_memory_t *mem = (wg_memory_t *)userp;
78
79 if (0x7FFFFFF0 < mem->size || 0x7FFFFFF0 - mem->size < realsize) {
80 ERROR("integer overflow");
81 return 0;
82 }
83
84 mem->memory = (char *)realloc((void *)mem->memory, mem->size + realsize + 1);
85 if (mem->memory == NULL) {
86 /* out of memory! */
87 ERROR("wg_write_memory_cb: not enough memory (realloc returned NULL)");
88 return 0;
89 }
90
91 memcpy(&(mem->memory[mem->size]), contents, realsize);
92 mem->size += realsize;
93 mem->memory[mem->size] = 0;
94 return realsize;
95 } /* }}} size_t wg_write_memory_cb */
96
wg_get_authorization_header(wg_callback_t * cb)97 static char *wg_get_authorization_header(wg_callback_t *cb) { /* {{{ */
98 int status = 0;
99 char access_token[256];
100 char authorization_header[256];
101
102 assert((cb->auth != NULL) || gce_check());
103 if (cb->auth != NULL)
104 status = oauth_access_token(cb->auth, access_token, sizeof(access_token));
105 else
106 status = gce_access_token(cb->email, access_token, sizeof(access_token));
107 if (status != 0) {
108 ERROR("write_stackdriver plugin: Failed to get access token");
109 return NULL;
110 }
111
112 status = ssnprintf(authorization_header, sizeof(authorization_header),
113 "Authorization: Bearer %s", access_token);
114 if ((status < 1) || ((size_t)status >= sizeof(authorization_header)))
115 return NULL;
116
117 return strdup(authorization_header);
118 } /* }}} char *wg_get_authorization_header */
119
120 typedef struct {
121 int code;
122 char *message;
123 } api_error_t;
124
parse_api_error(char const * body)125 static api_error_t *parse_api_error(char const *body) {
126 char errbuf[1024];
127 yajl_val root = yajl_tree_parse(body, errbuf, sizeof(errbuf));
128 if (root == NULL) {
129 ERROR("write_stackdriver plugin: yajl_tree_parse failed: %s", errbuf);
130 return NULL;
131 }
132
133 api_error_t *err = calloc(1, sizeof(*err));
134 if (err == NULL) {
135 ERROR("write_stackdriver plugin: calloc failed");
136 yajl_tree_free(root);
137 return NULL;
138 }
139
140 yajl_val code = yajl_tree_get(root, (char const *[]){"error", "code", NULL},
141 yajl_t_number);
142 if (YAJL_IS_INTEGER(code)) {
143 err->code = YAJL_GET_INTEGER(code);
144 }
145
146 yajl_val message = yajl_tree_get(
147 root, (char const *[]){"error", "message", NULL}, yajl_t_string);
148 if (YAJL_IS_STRING(message)) {
149 char const *m = YAJL_GET_STRING(message);
150 if (m != NULL) {
151 err->message = strdup(m);
152 }
153 }
154
155 return err;
156 }
157
api_error_string(api_error_t * err,char * buffer,size_t buffer_size)158 static char *api_error_string(api_error_t *err, char *buffer,
159 size_t buffer_size) {
160 if (err == NULL) {
161 strncpy(buffer, "Unknown error (API error is NULL)", buffer_size);
162 } else if (err->message == NULL) {
163 ssnprintf(buffer, buffer_size, "API error %d", err->code);
164 } else {
165 ssnprintf(buffer, buffer_size, "API error %d: %s", err->code, err->message);
166 }
167
168 return buffer;
169 }
170 #define API_ERROR_STRING(err) api_error_string(err, (char[1024]){""}, 1024)
171
172 // do_post does a HTTP POST request, assuming a JSON payload and using OAuth
173 // authentication. Returns -1 on error and the HTTP status code otherwise.
174 // ret_content, if not NULL, will contain the server's response.
175 // If ret_content is provided and the server responds with a 4xx or 5xx error,
176 // an appropriate message will be logged.
do_post(wg_callback_t * cb,char const * url,void const * payload,wg_memory_t * ret_content)177 static int do_post(wg_callback_t *cb, char const *url, void const *payload,
178 wg_memory_t *ret_content) {
179 if (cb->curl == NULL) {
180 cb->curl = curl_easy_init();
181 if (cb->curl == NULL) {
182 ERROR("write_stackdriver plugin: curl_easy_init() failed");
183 return -1;
184 }
185
186 curl_easy_setopt(cb->curl, CURLOPT_ERRORBUFFER, cb->curl_errbuf);
187 curl_easy_setopt(cb->curl, CURLOPT_NOSIGNAL, 1L);
188 }
189
190 curl_easy_setopt(cb->curl, CURLOPT_POST, 1L);
191 curl_easy_setopt(cb->curl, CURLOPT_URL, url);
192
193 long timeout_ms = 2 * CDTIME_T_TO_MS(plugin_get_interval());
194 if (timeout_ms < 10000) {
195 timeout_ms = 10000;
196 }
197 curl_easy_setopt(cb->curl, CURLOPT_TIMEOUT_MS, timeout_ms);
198
199 /* header */
200 char *auth_header = wg_get_authorization_header(cb);
201 if (auth_header == NULL) {
202 ERROR("write_stackdriver plugin: getting access token failed with");
203 return -1;
204 }
205
206 struct curl_slist *headers =
207 curl_slist_append(NULL, "Content-Type: application/json");
208 headers = curl_slist_append(headers, auth_header);
209 curl_easy_setopt(cb->curl, CURLOPT_HTTPHEADER, headers);
210
211 curl_easy_setopt(cb->curl, CURLOPT_POSTFIELDS, payload);
212
213 curl_easy_setopt(cb->curl, CURLOPT_WRITEFUNCTION,
214 ret_content ? wg_write_memory_cb : NULL);
215 curl_easy_setopt(cb->curl, CURLOPT_WRITEDATA, ret_content);
216
217 int status = curl_easy_perform(cb->curl);
218
219 /* clean up that has to happen in any case */
220 curl_slist_free_all(headers);
221 sfree(auth_header);
222 curl_easy_setopt(cb->curl, CURLOPT_HTTPHEADER, NULL);
223 curl_easy_setopt(cb->curl, CURLOPT_WRITEFUNCTION, NULL);
224 curl_easy_setopt(cb->curl, CURLOPT_WRITEDATA, NULL);
225
226 if (status != CURLE_OK) {
227 ERROR("write_stackdriver plugin: POST %s failed: %s", url, cb->curl_errbuf);
228 if (ret_content != NULL) {
229 sfree(ret_content->memory);
230 ret_content->size = 0;
231 }
232 return -1;
233 }
234
235 long http_code = 0;
236 curl_easy_getinfo(cb->curl, CURLINFO_RESPONSE_CODE, &http_code);
237
238 if (ret_content != NULL) {
239 if ((http_code >= 400) && (http_code < 500)) {
240 ERROR("write_stackdriver plugin: POST %s: %s", url,
241 API_ERROR_STRING(parse_api_error(ret_content->memory)));
242 } else if (http_code >= 500) {
243 WARNING("write_stackdriver plugin: POST %s: %s", url,
244 ret_content->memory);
245 }
246 }
247
248 return (int)http_code;
249 } /* int do_post */
250
wg_call_metricdescriptor_create(wg_callback_t * cb,char const * payload)251 static int wg_call_metricdescriptor_create(wg_callback_t *cb,
252 char const *payload) {
253 char url[1024];
254 ssnprintf(url, sizeof(url), "%s/projects/%s/metricDescriptors", cb->url,
255 cb->project);
256 wg_memory_t response = {0};
257
258 int status = do_post(cb, url, payload, &response);
259 if (status == -1) {
260 ERROR("write_stackdriver plugin: POST %s failed", url);
261 return -1;
262 }
263 sfree(response.memory);
264
265 if (status != 200) {
266 ERROR("write_stackdriver plugin: POST %s: unexpected response code: got "
267 "%d, want 200",
268 url, status);
269 return -1;
270 }
271 return 0;
272 } /* int wg_call_metricdescriptor_create */
273
wg_call_timeseries_write(wg_callback_t * cb,char const * payload)274 static int wg_call_timeseries_write(wg_callback_t *cb, char const *payload) {
275 char url[1024];
276 ssnprintf(url, sizeof(url), "%s/projects/%s/timeSeries", cb->url,
277 cb->project);
278 wg_memory_t response = {0};
279
280 int status = do_post(cb, url, payload, &response);
281 if (status == -1) {
282 ERROR("write_stackdriver plugin: POST %s failed", url);
283 return -1;
284 }
285 sfree(response.memory);
286
287 if (status != 200) {
288 ERROR("write_stackdriver plugin: POST %s: unexpected response code: got "
289 "%d, want 200",
290 url, status);
291 return -1;
292 }
293 return 0;
294 } /* int wg_call_timeseries_write */
295
wg_reset_buffer(wg_callback_t * cb)296 static void wg_reset_buffer(wg_callback_t *cb) /* {{{ */
297 {
298 cb->timeseries_count = 0;
299 cb->send_buffer_init_time = cdtime();
300 } /* }}} wg_reset_buffer */
301
wg_callback_init(wg_callback_t * cb)302 static int wg_callback_init(wg_callback_t *cb) /* {{{ */
303 {
304 if (cb->curl != NULL)
305 return 0;
306
307 cb->formatter = sd_output_create(cb->resource);
308 if (cb->formatter == NULL) {
309 ERROR("write_stackdriver plugin: sd_output_create failed.");
310 return -1;
311 }
312
313 cb->curl = curl_easy_init();
314 if (cb->curl == NULL) {
315 ERROR("write_stackdriver plugin: curl_easy_init failed.");
316 return -1;
317 }
318
319 curl_easy_setopt(cb->curl, CURLOPT_NOSIGNAL, 1L);
320 curl_easy_setopt(cb->curl, CURLOPT_USERAGENT,
321 PACKAGE_NAME "/" PACKAGE_VERSION);
322 curl_easy_setopt(cb->curl, CURLOPT_ERRORBUFFER, cb->curl_errbuf);
323 wg_reset_buffer(cb);
324
325 return 0;
326 } /* }}} int wg_callback_init */
327
wg_flush_nolock(cdtime_t timeout,wg_callback_t * cb)328 static int wg_flush_nolock(cdtime_t timeout, wg_callback_t *cb) /* {{{ */
329 {
330 if (cb->timeseries_count == 0) {
331 cb->send_buffer_init_time = cdtime();
332 return 0;
333 }
334
335 /* timeout == 0 => flush unconditionally */
336 if (timeout > 0) {
337 cdtime_t now = cdtime();
338
339 if ((cb->send_buffer_init_time + timeout) > now)
340 return 0;
341 }
342
343 char *payload = sd_output_reset(cb->formatter);
344 int status = wg_call_timeseries_write(cb, payload);
345 wg_reset_buffer(cb);
346 return status;
347 } /* }}} wg_flush_nolock */
348
wg_flush(cdtime_t timeout,const char * identifier,user_data_t * user_data)349 static int wg_flush(cdtime_t timeout, /* {{{ */
350 const char *identifier __attribute__((unused)),
351 user_data_t *user_data) {
352 wg_callback_t *cb;
353 int status;
354
355 if (user_data == NULL)
356 return -EINVAL;
357
358 cb = user_data->data;
359
360 pthread_mutex_lock(&cb->lock);
361
362 if (cb->curl == NULL) {
363 status = wg_callback_init(cb);
364 if (status != 0) {
365 ERROR("write_stackdriver plugin: wg_callback_init failed.");
366 pthread_mutex_unlock(&cb->lock);
367 return -1;
368 }
369 }
370
371 status = wg_flush_nolock(timeout, cb);
372 pthread_mutex_unlock(&cb->lock);
373
374 return status;
375 } /* }}} int wg_flush */
376
wg_callback_free(void * data)377 static void wg_callback_free(void *data) /* {{{ */
378 {
379 wg_callback_t *cb = data;
380 if (cb == NULL)
381 return;
382
383 sd_output_destroy(cb->formatter);
384 cb->formatter = NULL;
385
386 sfree(cb->email);
387 sfree(cb->project);
388 sfree(cb->url);
389
390 oauth_destroy(cb->auth);
391 if (cb->curl) {
392 curl_easy_cleanup(cb->curl);
393 }
394
395 sfree(cb);
396 } /* }}} void wg_callback_free */
397
wg_metric_descriptors_create(wg_callback_t * cb,const data_set_t * ds,const value_list_t * vl)398 static int wg_metric_descriptors_create(wg_callback_t *cb, const data_set_t *ds,
399 const value_list_t *vl) {
400 /* {{{ */
401 for (size_t i = 0; i < ds->ds_num; i++) {
402 char buffer[4096];
403
404 int status = sd_format_metric_descriptor(buffer, sizeof(buffer), ds, vl, i);
405 if (status != 0) {
406 ERROR("write_stackdriver plugin: sd_format_metric_descriptor failed "
407 "with status "
408 "%d",
409 status);
410 return status;
411 }
412
413 status = wg_call_metricdescriptor_create(cb, buffer);
414 if (status != 0) {
415 ERROR("write_stackdriver plugin: wg_call_metricdescriptor_create failed "
416 "with "
417 "status %d",
418 status);
419 return status;
420 }
421 }
422
423 return sd_output_register_metric(cb->formatter, ds, vl);
424 } /* }}} int wg_metric_descriptors_create */
425
wg_write(const data_set_t * ds,const value_list_t * vl,user_data_t * user_data)426 static int wg_write(const data_set_t *ds, const value_list_t *vl, /* {{{ */
427 user_data_t *user_data) {
428 wg_callback_t *cb = user_data->data;
429 if (cb == NULL)
430 return EINVAL;
431
432 pthread_mutex_lock(&cb->lock);
433
434 if (cb->curl == NULL) {
435 int status = wg_callback_init(cb);
436 if (status != 0) {
437 ERROR("write_stackdriver plugin: wg_callback_init failed.");
438 pthread_mutex_unlock(&cb->lock);
439 return status;
440 }
441 }
442
443 int status;
444 while (42) {
445 status = sd_output_add(cb->formatter, ds, vl);
446 if (status == 0) { /* success */
447 break;
448 } else if (status == ENOBUFS) { /* success, flush */
449 wg_flush_nolock(0, cb);
450 status = 0;
451 break;
452 } else if (status == EEXIST) {
453 /* metric already in the buffer; flush and retry */
454 wg_flush_nolock(0, cb);
455 continue;
456 } else if (status == ENOENT) {
457 /* new metric, create metric descriptor first */
458 status = wg_metric_descriptors_create(cb, ds, vl);
459 if (status != 0) {
460 break;
461 }
462 continue;
463 } else {
464 break;
465 }
466 }
467
468 if (status == 0) {
469 cb->timeseries_count++;
470 }
471
472 pthread_mutex_unlock(&cb->lock);
473 return status;
474 } /* }}} int wg_write */
475
wg_check_scope(char const * email)476 static void wg_check_scope(char const *email) /* {{{ */
477 {
478 char *scope = gce_scope(email);
479 if (scope == NULL) {
480 WARNING("write_stackdriver plugin: Unable to determine scope of this "
481 "instance.");
482 return;
483 }
484
485 if (strstr(scope, MONITORING_SCOPE) == NULL) {
486 size_t scope_len;
487
488 /* Strip trailing newline characers for printing. */
489 scope_len = strlen(scope);
490 while ((scope_len > 0) && (iscntrl((int)scope[scope_len - 1])))
491 scope[--scope_len] = 0;
492
493 WARNING("write_stackdriver plugin: The determined scope of this instance "
494 "(\"%s\") does not contain the monitoring scope (\"%s\"). You need "
495 "to add this scope to the list of scopes passed to gcutil with "
496 "--service_account_scopes when creating the instance. "
497 "Alternatively, to use this plugin on an instance which does not "
498 "have this scope, use a Service Account.",
499 scope, MONITORING_SCOPE);
500 }
501
502 sfree(scope);
503 } /* }}} void wg_check_scope */
504
wg_config_resource(oconfig_item_t * ci,wg_callback_t * cb)505 static int wg_config_resource(oconfig_item_t *ci, wg_callback_t *cb) /* {{{ */
506 {
507 if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_STRING)) {
508 ERROR("write_stackdriver plugin: The \"%s\" option requires exactly one "
509 "string "
510 "argument.",
511 ci->key);
512 return EINVAL;
513 }
514 char *resource_type = ci->values[0].value.string;
515
516 if (cb->resource != NULL) {
517 sd_resource_destroy(cb->resource);
518 }
519
520 cb->resource = sd_resource_create(resource_type);
521 if (cb->resource == NULL) {
522 ERROR("write_stackdriver plugin: sd_resource_create(\"%s\") failed.",
523 resource_type);
524 return ENOMEM;
525 }
526
527 for (int i = 0; i < ci->children_num; i++) {
528 oconfig_item_t *child = ci->children + i;
529
530 if (strcasecmp("Label", child->key) == 0) {
531 if ((child->values_num != 2) ||
532 (child->values[0].type != OCONFIG_TYPE_STRING) ||
533 (child->values[1].type != OCONFIG_TYPE_STRING)) {
534 ERROR("write_stackdriver plugin: The \"Label\" option needs exactly "
535 "two string arguments.");
536 continue;
537 }
538
539 sd_resource_add_label(cb->resource, child->values[0].value.string,
540 child->values[1].value.string);
541 }
542 }
543
544 return 0;
545 } /* }}} int wg_config_resource */
546
wg_config(oconfig_item_t * ci)547 static int wg_config(oconfig_item_t *ci) /* {{{ */
548 {
549 if (ci == NULL) {
550 return EINVAL;
551 }
552
553 wg_callback_t *cb = calloc(1, sizeof(*cb));
554 if (cb == NULL) {
555 ERROR("write_stackdriver plugin: calloc failed.");
556 return ENOMEM;
557 }
558 cb->url = strdup(GCM_API_URL);
559 pthread_mutex_init(&cb->lock, /* attr = */ NULL);
560
561 char *credential_file = NULL;
562
563 for (int i = 0; i < ci->children_num; i++) {
564 oconfig_item_t *child = ci->children + i;
565 if (strcasecmp("Project", child->key) == 0)
566 cf_util_get_string(child, &cb->project);
567 else if (strcasecmp("Email", child->key) == 0)
568 cf_util_get_string(child, &cb->email);
569 else if (strcasecmp("Url", child->key) == 0)
570 cf_util_get_string(child, &cb->url);
571 else if (strcasecmp("CredentialFile", child->key) == 0)
572 cf_util_get_string(child, &credential_file);
573 else if (strcasecmp("Resource", child->key) == 0)
574 wg_config_resource(child, cb);
575 else {
576 ERROR("write_stackdriver plugin: Invalid configuration option: %s.",
577 child->key);
578 wg_callback_free(cb);
579 return EINVAL;
580 }
581 }
582
583 /* Set up authentication */
584 /* Option 1: Credentials file given => use service account */
585 if (credential_file != NULL) {
586 oauth_google_t cfg =
587 oauth_create_google_file(credential_file, MONITORING_SCOPE);
588 if (cfg.oauth == NULL) {
589 ERROR("write_stackdriver plugin: oauth_create_google_file failed");
590 wg_callback_free(cb);
591 return EINVAL;
592 }
593 cb->auth = cfg.oauth;
594
595 if (cb->project == NULL) {
596 cb->project = cfg.project_id;
597 INFO("write_stackdriver plugin: Automatically detected project ID: "
598 "\"%s\"",
599 cb->project);
600 } else {
601 sfree(cfg.project_id);
602 }
603 }
604 /* Option 2: Look for credentials in well-known places */
605 if (cb->auth == NULL) {
606 oauth_google_t cfg = oauth_create_google_default(MONITORING_SCOPE);
607 cb->auth = cfg.oauth;
608
609 if (cb->project == NULL) {
610 cb->project = cfg.project_id;
611 INFO("write_stackdriver plugin: Automatically detected project ID: "
612 "\"%s\"",
613 cb->project);
614 } else {
615 sfree(cfg.project_id);
616 }
617 }
618
619 if ((cb->auth != NULL) && (cb->email != NULL)) {
620 NOTICE("write_stackdriver plugin: A service account email was configured "
621 "but is "
622 "not used for authentication because %s used instead.",
623 (credential_file != NULL) ? "a credential file was"
624 : "application default credentials were");
625 }
626
627 /* Option 3: Running on GCE => use metadata service */
628 if ((cb->auth == NULL) && gce_check()) {
629 wg_check_scope(cb->email);
630 } else if (cb->auth == NULL) {
631 ERROR("write_stackdriver plugin: Unable to determine credentials. Please "
632 "either "
633 "specify the \"Credentials\" option or set up Application Default "
634 "Credentials.");
635 wg_callback_free(cb);
636 return EINVAL;
637 }
638
639 if ((cb->project == NULL) && gce_check()) {
640 cb->project = gce_project_id();
641 }
642 if (cb->project == NULL) {
643 ERROR("write_stackdriver plugin: Unable to determine the project number. "
644 "Please specify the \"Project\" option manually.");
645 wg_callback_free(cb);
646 return EINVAL;
647 }
648
649 if ((cb->resource == NULL) && gce_check()) {
650 /* TODO(octo): add error handling */
651 cb->resource = sd_resource_create("gce_instance");
652 sd_resource_add_label(cb->resource, "project_id", gce_project_id());
653 sd_resource_add_label(cb->resource, "instance_id", gce_instance_id());
654 sd_resource_add_label(cb->resource, "zone", gce_zone());
655 }
656 if (cb->resource == NULL) {
657 /* TODO(octo): add error handling */
658 cb->resource = sd_resource_create("global");
659 sd_resource_add_label(cb->resource, "project_id", cb->project);
660 }
661
662 DEBUG("write_stackdriver plugin: Registering write callback with URL %s",
663 cb->url);
664 assert((cb->auth != NULL) || gce_check());
665
666 user_data_t user_data = {
667 .data = cb,
668 };
669 plugin_register_flush("write_stackdriver", wg_flush, &user_data);
670
671 user_data.free_func = wg_callback_free;
672 plugin_register_write("write_stackdriver", wg_write, &user_data);
673
674 return 0;
675 } /* }}} int wg_config */
676
wg_init(void)677 static int wg_init(void) {
678 /* {{{ */
679 /* Call this while collectd is still single-threaded to avoid
680 * initialization issues in libgcrypt. */
681 curl_global_init(CURL_GLOBAL_SSL);
682
683 return 0;
684 } /* }}} int wg_init */
685
module_register(void)686 void module_register(void) /* {{{ */
687 {
688 plugin_register_complex_config("write_stackdriver", wg_config);
689 plugin_register_init("write_stackdriver", wg_init);
690 } /* }}} void module_register */
691