write_stackdriver plugin: Centralize HTTP calls, add API error handling.
authorFlorian Forster <octo@collectd.org>
Sun, 7 Oct 2018 08:21:47 +0000 (10:21 +0200)
committerFlorian Forster <octo@collectd.org>
Mon, 8 Oct 2018 08:24:58 +0000 (10:24 +0200)
This change implements do_post(), a function for doing POST requests.
In case of a user error (a HTTP 4xx status code) the reply is assumed to
be an error and is parsed accordingly.

src/write_stackdriver.c

index bb24e23..eb60ea9 100644 (file)
@@ -31,6 +31,7 @@
 
 #include <curl/curl.h>
 #include <pthread.h>
+#include <yajl/yajl_tree.h>
 
 /*
  * Private variables
@@ -116,142 +117,175 @@ static char *wg_get_authorization_header(wg_callback_t *cb) { /* {{{ */
   return strdup(authorization_header);
 } /* }}} char *wg_get_authorization_header */
 
-static int wg_call_metricdescriptor_create(wg_callback_t *cb,
-                                           char const *payload) {
-  /* {{{ */
-  char final_url[1024];
-  int status =
-      snprintf(final_url, sizeof(final_url), "%s/projects/%s/metricDescriptors",
-               cb->url, cb->project);
-  if ((status < 1) || ((size_t)status >= sizeof(final_url)))
-    return -1;
+typedef struct {
+  int code;
+  char *message;
+} api_error_t;
 
-  char *authorization_header = wg_get_authorization_header(cb);
-  if (authorization_header == NULL)
-    return -1;
-
-  struct curl_slist *headers = NULL;
-  headers = curl_slist_append(headers, "Content-Type: application/json");
-  headers = curl_slist_append(headers, authorization_header);
+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;
+  }
 
-  CURL *curl = curl_easy_init();
-  if (!curl) {
-    ERROR("write_stackdriver plugin: curl_easy_init failed.");
-    curl_slist_free_all(headers);
-    sfree(authorization_header);
-    return -1;
+  api_error_t *err = calloc(1, sizeof(*err));
+  if (err == NULL) {
+    ERROR("write_stackdriver plugin: calloc failed");
+    yajl_tree_free(root);
+    return NULL;
   }
 
-  curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
-  curl_easy_setopt(cb->curl, CURLOPT_USERAGENT,
-                   PACKAGE_NAME "/" PACKAGE_VERSION);
-  char curl_errbuf[CURL_ERROR_SIZE];
-  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
-  curl_easy_setopt(curl, CURLOPT_URL, final_url);
-  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
-  curl_easy_setopt(curl, CURLOPT_POST, 1L);
-  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
-
-  wg_memory_t res = {
-      .memory = NULL, .size = 0,
-  };
-  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, wg_write_memory_cb);
-  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res);
+  yajl_val code = yajl_tree_get(root, (char const *[]){"error", "code", NULL},
+                                yajl_t_number);
+  if (code != NULL) {
+    err->code = YAJL_GET_INTEGER(code);
+  }
 
-  status = curl_easy_perform(curl);
-  if (status != CURLE_OK) {
-    ERROR(
-        "write_stackdriver plugin: curl_easy_perform failed with status %d: %s",
-        status, curl_errbuf);
-    sfree(res.memory);
-    curl_easy_cleanup(curl);
-    curl_slist_free_all(headers);
-    sfree(authorization_header);
-    return -1;
+  yajl_val message = yajl_tree_get(
+      root, (char const *[]){"error", "message", NULL}, yajl_t_string);
+  if (message != NULL) {
+    err->message = strdup(YAJL_GET_STRING(message));
   }
 
-  long http_code = 0;
-  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
-  if ((http_code < 200) || (http_code >= 300)) {
-    ERROR("write_stackdriver plugin: POST request to %s failed: HTTP error %ld",
-          final_url, http_code);
-    INFO("write_stackdriver plugin: Server replied: %s", res.memory);
-    sfree(res.memory);
-    curl_easy_cleanup(curl);
-    curl_slist_free_all(headers);
-    sfree(authorization_header);
-    return -1;
+  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);
   }
 
-  sfree(res.memory);
-  curl_easy_cleanup(curl);
-  curl_slist_free_all(headers);
-  sfree(authorization_header);
-  return 0;
-} /* }}} int wg_call_metricdescriptor_create */
+  return buffer;
+}
+#define API_ERROR_STRING(err) api_error_string(err, (char[1024]){""}, 1024)
 
-static void wg_reset_buffer(wg_callback_t *cb) /* {{{ */
-{
-  cb->timeseries_count = 0;
-  cb->send_buffer_init_time = cdtime();
-} /* }}} wg_reset_buffer */
+// 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;
+    }
 
-static int wg_call_timeseries_write(wg_callback_t *cb,
-                                    char const *payload) /* {{{ */
-{
-  char final_url[1024];
-  int status = snprintf(final_url, sizeof(final_url),
-                        "%s/projects/%s/timeSeries", cb->url, cb->project);
-  if ((status < 1) || ((size_t)status >= sizeof(final_url)))
-    return -1;
+    curl_easy_setopt(cb->curl, CURLOPT_ERRORBUFFER, cb->curl_errbuf);
+    curl_easy_setopt(cb->curl, CURLOPT_NOSIGNAL, 1L);
+  }
 
-  char *authorization_header = wg_get_authorization_header(cb);
-  if (authorization_header == NULL)
-    return -1;
+  curl_easy_setopt(cb->curl, CURLOPT_POST, 1L);
+  curl_easy_setopt(cb->curl, CURLOPT_URL, url);
 
-  struct curl_slist *headers = NULL;
-  headers = curl_slist_append(headers, authorization_header);
-  headers = curl_slist_append(headers, "Content-Type: application/json");
+  /* header */
+  char *auth_header = wg_get_authorization_header(cb);
+  if (auth_header == NULL) {
+    ERROR("write_stackdriver plugin: getting access token failed with");
+    return -1;
+  }
 
-  curl_easy_setopt(cb->curl, CURLOPT_URL, final_url);
+  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_POST, 1L);
+
   curl_easy_setopt(cb->curl, CURLOPT_POSTFIELDS, payload);
 
-  wg_memory_t res = {
-      .memory = NULL, .size = 0,
-  };
-  curl_easy_setopt(cb->curl, CURLOPT_WRITEFUNCTION, wg_write_memory_cb);
-  curl_easy_setopt(cb->curl, CURLOPT_WRITEDATA, &res);
+  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);
 
-  status = curl_easy_perform(cb->curl);
   if (status != CURLE_OK) {
-    ERROR(
-        "write_stackdriver plugin: curl_easy_perform failed with status %d: %s",
-        status, cb->curl_errbuf);
-    sfree(res.memory);
-    curl_slist_free_all(headers);
-    sfree(authorization_header);
+    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 ((http_code < 200) || (http_code >= 300)) {
-    ERROR("write_stackdriver plugin: POST request to %s failed: HTTP error %ld",
-          final_url, http_code);
-    INFO("write_stackdriver plugin: Server replied: %s", res.memory);
-    sfree(res.memory);
-    curl_slist_free_all(headers);
-    sfree(authorization_header);
+
+  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);
 
-  sfree(res.memory);
-  curl_slist_free_all(headers);
-  sfree(authorization_header);
-  return status;
-} /* }}} wg_call_timeseries_write */
+  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) /* {{{ */
 {
@@ -553,9 +587,9 @@ static int wg_config(oconfig_item_t *ci) /* {{{ */
 
     if (cb->project == NULL) {
       cb->project = cfg.project_id;
-      INFO(
-          "write_stackdriver plugin: Automatically detected project ID: \"%s\"",
-          cb->project);
+      INFO("write_stackdriver plugin: Automatically detected project ID: "
+           "\"%s\"",
+           cb->project);
     } else {
       sfree(cfg.project_id);
     }
@@ -567,9 +601,9 @@ static int wg_config(oconfig_item_t *ci) /* {{{ */
 
     if (cb->project == NULL) {
       cb->project = cfg.project_id;
-      INFO(
-          "write_stackdriver plugin: Automatically detected project ID: \"%s\"",
-          cb->project);
+      INFO("write_stackdriver plugin: Automatically detected project ID: "
+           "\"%s\"",
+           cb->project);
     } else {
       sfree(cfg.project_id);
     }