curl_json plugin: Query the URL before trying to print it.
[collectd.git] / src / curl_json.c
1 /**
2  * collectd - src/curl_json.c
3  * Copyright (C) 2009       Doug MacEachern
4  * Copyright (C) 2006-2010  Florian octo Forster
5  *
6  * This program is free software; you can redistribute it and/or modify it
7  * under the terms of the GNU General Public License as published by the
8  * Free Software Foundation; only version 2 of the License is applicable.
9  *
10  * This program is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
18  *
19  * Authors:
20  *   Doug MacEachern <dougm at hyperic.com>
21  *   Florian octo Forster <octo at verplant.org>
22  **/
23
24 #include "collectd.h"
25 #include "common.h"
26 #include "plugin.h"
27 #include "configfile.h"
28 #include "utils_avltree.h"
29
30 #include <curl/curl.h>
31 #include <yajl/yajl_parse.h>
32
33 #define CJ_DEFAULT_HOST "localhost"
34 #define CJ_KEY_MAGIC 0x43484b59UL /* CHKY */
35 #define CJ_IS_KEY(key) ((key)->magic == CJ_KEY_MAGIC)
36 #define CJ_ANY "*"
37 #define COUCH_MIN(x,y) ((x) < (y) ? (x) : (y))
38
39 struct cj_key_s;
40 typedef struct cj_key_s cj_key_t;
41 struct cj_key_s /* {{{ */
42 {
43   char *path;
44   char *type;
45   char *instance;
46   unsigned long magic;
47 };
48 /* }}} */
49
50 struct cj_s /* {{{ */
51 {
52   char *instance;
53   char *host;
54
55   char *url;
56   char *user;
57   char *pass;
58   char *credentials;
59   _Bool verify_peer;
60   _Bool verify_host;
61   char *cacert;
62
63   CURL *curl;
64   char curl_errbuf[CURL_ERROR_SIZE];
65
66   yajl_handle yajl;
67   c_avl_tree_t *tree;
68   cj_key_t *key;
69   int depth;
70   struct {
71     union {
72       c_avl_tree_t *tree;
73       cj_key_t *key;
74     };
75     char name[DATA_MAX_NAME_LEN];
76   } state[YAJL_MAX_DEPTH];
77 };
78 typedef struct cj_s cj_t; /* }}} */
79
80 static int cj_read (user_data_t *ud);
81 static int cj_curl_perform (cj_t *db, CURL *curl);
82 static void cj_submit (cj_t *db, cj_key_t *key, value_t *value);
83
84 static size_t cj_curl_callback (void *buf, /* {{{ */
85     size_t size, size_t nmemb, void *user_data)
86 {
87   cj_t *db;
88   size_t len;
89   yajl_status status;
90
91   len = size * nmemb;
92
93   if (len <= 0)
94     return (len);
95
96   db = user_data;
97   if (db == NULL)
98     return (0);
99
100   status = yajl_parse(db->yajl, (unsigned char *)buf, len);
101   if ((status != yajl_status_ok)
102       && (status != yajl_status_insufficient_data))
103   {
104     unsigned char *msg =
105       yajl_get_error(db->yajl, /* verbose = */ 1,
106           /* jsonText = */ (unsigned char *) buf, (unsigned int) len);
107     ERROR ("curl_json plugin: yajl_parse failed: %s", msg);
108     yajl_free_error(db->yajl, msg);
109     return (0); /* abort write callback */
110   }
111
112   return (len);
113 } /* }}} size_t cj_curl_callback */
114
115 static int cj_get_type (cj_key_t *key)
116 {
117   const data_set_t *ds;
118
119   ds = plugin_get_ds (key->type);
120   if (ds == NULL)
121     return -1; /* let plugin_write do the complaining */
122   else
123     return ds->ds[0].type; /* XXX support ds->ds_len > 1 */
124 }
125
126 /* yajl callbacks */
127 #define CJ_CB_ABORT    0
128 #define CJ_CB_CONTINUE 1
129
130 /* "number" may not be null terminated, so copy it into a buffer before
131  * parsing. */
132 static int cj_cb_number (void *ctx,
133     const char *number, unsigned int number_len)
134 {
135   char buffer[number_len + 1];
136
137   cj_t *db = (cj_t *)ctx;
138   cj_key_t *key = db->state[db->depth].key;
139   value_t vt;
140   int type;
141   int status;
142
143   if ((key == NULL) || !CJ_IS_KEY (key))
144     return (CJ_CB_CONTINUE);
145
146   memcpy (buffer, number, number_len);
147   buffer[sizeof (buffer) - 1] = 0;
148
149   type = cj_get_type (key);
150   status = parse_value (buffer, &vt, type);
151   if (status != 0)
152   {
153     NOTICE ("curl_json plugin: Unable to parse number: \"%s\"", buffer);
154     return (CJ_CB_CONTINUE);
155   }
156
157   cj_submit (db, key, &vt);
158   return (CJ_CB_CONTINUE);
159 } /* int cj_cb_number */
160
161 static int cj_cb_map_key (void *ctx, const unsigned char *val,
162                             unsigned int len)
163 {
164   cj_t *db = (cj_t *)ctx;
165   c_avl_tree_t *tree;
166
167   tree = db->state[db->depth-1].tree;
168
169   if (tree != NULL)
170   {
171     cj_key_t *value;
172     char *name;
173
174     name = db->state[db->depth].name;
175     len = COUCH_MIN(len, sizeof (db->state[db->depth].name)-1);
176     sstrncpy (name, (char *)val, len+1);
177
178     if (c_avl_get (tree, name, (void *) &value) == 0)
179       db->state[db->depth].key = value;
180     else if (c_avl_get (tree, CJ_ANY, (void *) &value) == 0)
181       db->state[db->depth].key = value;
182     else
183       db->state[db->depth].key = NULL;
184   }
185
186   return (CJ_CB_CONTINUE);
187 }
188
189 static int cj_cb_string (void *ctx, const unsigned char *val,
190                            unsigned int len)
191 {
192   cj_t *db = (cj_t *)ctx;
193   char str[len + 1];
194
195   /* Create a null-terminated version of the string. */
196   memcpy (str, val, len);
197   str[len] = 0;
198
199   /* No configuration for this string -> simply return. */
200   if (db->state[db->depth].key == NULL)
201     return (CJ_CB_CONTINUE);
202
203   if (!CJ_IS_KEY (db->state[db->depth].key))
204   {
205     NOTICE ("curl_json plugin: Found string \"%s\", but the configuration "
206         "expects a map here.", str);
207     return (CJ_CB_CONTINUE);
208   }
209
210   /* Handle the string as if it was a number. */
211   return (cj_cb_number (ctx, (const char *) val, len));
212 } /* int cj_cb_string */
213
214 static int cj_cb_start (void *ctx)
215 {
216   cj_t *db = (cj_t *)ctx;
217   if (++db->depth >= YAJL_MAX_DEPTH)
218   {
219     ERROR ("curl_json plugin: %s depth exceeds max, aborting.", db->url);
220     return (CJ_CB_ABORT);
221   }
222   return (CJ_CB_CONTINUE);
223 }
224
225 static int cj_cb_end (void *ctx)
226 {
227   cj_t *db = (cj_t *)ctx;
228   db->state[db->depth].tree = NULL;
229   --db->depth;
230   return (CJ_CB_CONTINUE);
231 }
232
233 static int cj_cb_start_map (void *ctx)
234 {
235   return cj_cb_start (ctx);
236 }
237
238 static int cj_cb_end_map (void *ctx)
239 {
240   return cj_cb_end (ctx);
241 }
242
243 static int cj_cb_start_array (void * ctx)
244 {
245   return cj_cb_start (ctx);
246 }
247
248 static int cj_cb_end_array (void * ctx)
249 {
250   return cj_cb_start (ctx);
251 }
252
253 static yajl_callbacks ycallbacks = {
254   NULL, /* null */
255   NULL, /* boolean */
256   NULL, /* integer */
257   NULL, /* double */
258   cj_cb_number,
259   cj_cb_string,
260   cj_cb_start_map,
261   cj_cb_map_key,
262   cj_cb_end_map,
263   cj_cb_start_array,
264   cj_cb_end_array
265 };
266
267 /* end yajl callbacks */
268
269 static void cj_key_free (cj_key_t *key) /* {{{ */
270 {
271   if (key == NULL)
272     return;
273
274   sfree (key->path);
275   sfree (key->type);
276   sfree (key->instance);
277
278   sfree (key);
279 } /* }}} void cj_key_free */
280
281 static void cj_tree_free (c_avl_tree_t *tree) /* {{{ */
282 {
283   char *name;
284   void *value;
285
286   while (c_avl_pick (tree, (void *) &name, (void *) &value) == 0)
287   {
288     cj_key_t *key = (cj_key_t *)value;
289
290     if (CJ_IS_KEY(key))
291       cj_key_free (key);
292     else
293       cj_tree_free ((c_avl_tree_t *)value);
294
295     sfree (name);
296   }
297
298   c_avl_destroy (tree);
299 } /* }}} void cj_tree_free */
300
301 static void cj_free (void *arg) /* {{{ */
302 {
303   cj_t *db;
304
305   DEBUG ("curl_json plugin: cj_free (arg = %p);", arg);
306
307   db = (cj_t *) arg;
308
309   if (db == NULL)
310     return;
311
312   if (db->curl != NULL)
313     curl_easy_cleanup (db->curl);
314   db->curl = NULL;
315
316   if (db->tree != NULL)
317     cj_tree_free (db->tree);
318   db->tree = NULL;
319
320   sfree (db->instance);
321   sfree (db->host);
322
323   sfree (db->url);
324   sfree (db->user);
325   sfree (db->pass);
326   sfree (db->credentials);
327   sfree (db->cacert);
328
329   sfree (db);
330 } /* }}} void cj_free */
331
332 /* Configuration handling functions {{{ */
333
334 static c_avl_tree_t *cj_avl_create(void)
335 {
336   return c_avl_create ((int (*) (const void *, const void *)) strcmp);
337 }
338
339 static int cj_config_add_key (cj_t *db, /* {{{ */
340                                    oconfig_item_t *ci)
341 {
342   cj_key_t *key;
343   int status;
344   int i;
345
346   if ((ci->values_num != 1)
347       || (ci->values[0].type != OCONFIG_TYPE_STRING))
348   {
349     WARNING ("curl_json plugin: The `Key' block "
350              "needs exactly one string argument.");
351     return (-1);
352   }
353
354   key = (cj_key_t *) malloc (sizeof (*key));
355   if (key == NULL)
356   {
357     ERROR ("curl_json plugin: malloc failed.");
358     return (-1);
359   }
360   memset (key, 0, sizeof (*key));
361   key->magic = CJ_KEY_MAGIC;
362
363   if (strcasecmp ("Key", ci->key) == 0)
364   {
365     status = cf_util_get_string (ci, &key->path);
366     if (status != 0)
367     {
368       sfree (key);
369       return (status);
370     }
371   }
372   else
373   {
374     ERROR ("curl_json plugin: cj_config: "
375            "Invalid key: %s", ci->key);
376     return (-1);
377   }
378
379   status = 0;
380   for (i = 0; i < ci->children_num; i++)
381   {
382     oconfig_item_t *child = ci->children + i;
383
384     if (strcasecmp ("Type", child->key) == 0)
385       status = cf_util_get_string (child, &key->type);
386     else if (strcasecmp ("Instance", child->key) == 0)
387       status = cf_util_get_string (child, &key->instance);
388     else
389     {
390       WARNING ("curl_json plugin: Option `%s' not allowed here.", child->key);
391       status = -1;
392     }
393
394     if (status != 0)
395       break;
396   } /* for (i = 0; i < ci->children_num; i++) */
397
398   while (status == 0)
399   {
400     if (key->type == NULL)
401     {
402       WARNING ("curl_json plugin: `Type' missing in `Key' block.");
403       status = -1;
404     }
405
406     break;
407   } /* while (status == 0) */
408
409   /* store path in a tree that will match the json map structure, example:
410    * "httpd/requests/count",
411    * "httpd/requests/current" ->
412    * { "httpd": { "requests": { "count": $key, "current": $key } } }
413    */
414   if (status == 0)
415   {
416     char *ptr;
417     char *name;
418     char ent[PATH_MAX];
419     c_avl_tree_t *tree;
420
421     if (db->tree == NULL)
422       db->tree = cj_avl_create();
423
424     tree = db->tree;
425     name = key->path;
426     ptr = key->path;
427     if (*ptr == '/')
428       ++ptr;
429
430     name = ptr;
431     while (*ptr)
432     {
433       if (*ptr == '/')
434       {
435         c_avl_tree_t *value;
436         int len;
437
438         len = ptr-name;
439         if (len == 0)
440           break;
441         sstrncpy (ent, name, len+1);
442
443         if (c_avl_get (tree, ent, (void *) &value) != 0)
444         {
445           value = cj_avl_create ();
446           c_avl_insert (tree, strdup (ent), value);
447         }
448
449         tree = value;
450         name = ptr+1;
451       }
452       ++ptr;
453     }
454     if (*name)
455       c_avl_insert (tree, strdup(name), key);
456     else
457     {
458       ERROR ("curl_json plugin: invalid key: %s", key->path);
459       status = -1;
460     }
461   }
462
463   return (status);
464 } /* }}} int cj_config_add_key */
465
466 static int cj_init_curl (cj_t *db) /* {{{ */
467 {
468   db->curl = curl_easy_init ();
469   if (db->curl == NULL)
470   {
471     ERROR ("curl_json plugin: curl_easy_init failed.");
472     return (-1);
473   }
474
475   curl_easy_setopt (db->curl, CURLOPT_WRITEFUNCTION, cj_curl_callback);
476   curl_easy_setopt (db->curl, CURLOPT_WRITEDATA, db);
477   curl_easy_setopt (db->curl, CURLOPT_USERAGENT,
478                     PACKAGE_NAME"/"PACKAGE_VERSION);
479   curl_easy_setopt (db->curl, CURLOPT_ERRORBUFFER, db->curl_errbuf);
480   curl_easy_setopt (db->curl, CURLOPT_URL, db->url);
481
482   if (db->user != NULL)
483   {
484     size_t credentials_size;
485
486     credentials_size = strlen (db->user) + 2;
487     if (db->pass != NULL)
488       credentials_size += strlen (db->pass);
489
490     db->credentials = (char *) malloc (credentials_size);
491     if (db->credentials == NULL)
492     {
493       ERROR ("curl_json plugin: malloc failed.");
494       return (-1);
495     }
496
497     ssnprintf (db->credentials, credentials_size, "%s:%s",
498                db->user, (db->pass == NULL) ? "" : db->pass);
499     curl_easy_setopt (db->curl, CURLOPT_USERPWD, db->credentials);
500   }
501
502   curl_easy_setopt (db->curl, CURLOPT_SSL_VERIFYPEER, (int) db->verify_peer);
503   curl_easy_setopt (db->curl, CURLOPT_SSL_VERIFYHOST,
504                     (int) (db->verify_host ? 2 : 0));
505   if (db->cacert != NULL)
506     curl_easy_setopt (db->curl, CURLOPT_CAINFO, db->cacert);
507
508   return (0);
509 } /* }}} int cj_init_curl */
510
511 static int cj_config_add_url (oconfig_item_t *ci) /* {{{ */
512 {
513   cj_t *db;
514   int status = 0;
515   int i;
516
517   if ((ci->values_num != 1)
518       || (ci->values[0].type != OCONFIG_TYPE_STRING))
519   {
520     WARNING ("curl_json plugin: The `URL' block "
521              "needs exactly one string argument.");
522     return (-1);
523   }
524
525   db = (cj_t *) malloc (sizeof (*db));
526   if (db == NULL)
527   {
528     ERROR ("curl_json plugin: malloc failed.");
529     return (-1);
530   }
531   memset (db, 0, sizeof (*db));
532
533   if (strcasecmp ("URL", ci->key) == 0)
534   {
535     status = cf_util_get_string (ci, &db->url);
536     if (status != 0)
537     {
538       sfree (db);
539       return (status);
540     }
541   }
542   else
543   {
544     ERROR ("curl_json plugin: cj_config: "
545            "Invalid key: %s", ci->key);
546     return (-1);
547   }
548
549   /* Fill the `cj_t' structure.. */
550   for (i = 0; i < ci->children_num; i++)
551   {
552     oconfig_item_t *child = ci->children + i;
553
554     if (strcasecmp ("Instance", child->key) == 0)
555       status = cf_util_get_string (child, &db->instance);
556     else if (strcasecmp ("Host", child->key) == 0)
557       status = cf_util_get_string (child, &db->host);
558     else if (strcasecmp ("User", child->key) == 0)
559       status = cf_util_get_string (child, &db->user);
560     else if (strcasecmp ("Password", child->key) == 0)
561       status = cf_util_get_string (child, &db->pass);
562     else if (strcasecmp ("VerifyPeer", child->key) == 0)
563       status = cf_util_get_boolean (child, &db->verify_peer);
564     else if (strcasecmp ("VerifyHost", child->key) == 0)
565       status = cf_util_get_boolean (child, &db->verify_host);
566     else if (strcasecmp ("CACert", child->key) == 0)
567       status = cf_util_get_string (child, &db->cacert);
568     else if (strcasecmp ("Key", child->key) == 0)
569       status = cj_config_add_key (db, child);
570     else
571     {
572       WARNING ("curl_json plugin: Option `%s' not allowed here.", child->key);
573       status = -1;
574     }
575
576     if (status != 0)
577       break;
578   }
579
580   if (status == 0)
581   {
582     if (db->tree == NULL)
583     {
584       WARNING ("curl_json plugin: No (valid) `Key' block "
585                "within `URL' block `%s'.", db->url);
586       status = -1;
587     }
588     if (status == 0)
589       status = cj_init_curl (db);
590   }
591
592   /* If all went well, register this database for reading */
593   if (status == 0)
594   {
595     user_data_t ud;
596     char cb_name[DATA_MAX_NAME_LEN];
597
598     if (db->instance == NULL)
599       db->instance = strdup("default");
600
601     DEBUG ("curl_json plugin: Registering new read callback: %s",
602            db->instance);
603
604     memset (&ud, 0, sizeof (ud));
605     ud.data = (void *) db;
606     ud.free_func = cj_free;
607
608     ssnprintf (cb_name, sizeof (cb_name), "curl_json-%s-%s",
609                db->instance, db->url);
610
611     plugin_register_complex_read (/* group = */ NULL, cb_name, cj_read,
612                                   /* interval = */ NULL, &ud);
613   }
614   else
615   {
616     cj_free (db);
617     return (-1);
618   }
619
620   return (0);
621 }
622  /* }}} int cj_config_add_database */
623
624 static int cj_config (oconfig_item_t *ci) /* {{{ */
625 {
626   int success;
627   int errors;
628   int status;
629   int i;
630
631   success = 0;
632   errors = 0;
633
634   for (i = 0; i < ci->children_num; i++)
635   {
636     oconfig_item_t *child = ci->children + i;
637
638     if (strcasecmp ("URL", child->key) == 0)
639     {
640       status = cj_config_add_url (child);
641       if (status == 0)
642         success++;
643       else
644         errors++;
645     }
646     else
647     {
648       WARNING ("curl_json plugin: Option `%s' not allowed here.", child->key);
649       errors++;
650     }
651   }
652
653   if ((success == 0) && (errors > 0))
654   {
655     ERROR ("curl_json plugin: All statements failed.");
656     return (-1);
657   }
658
659   return (0);
660 } /* }}} int cj_config */
661
662 /* }}} End of configuration handling functions */
663
664 static void cj_submit (cj_t *db, cj_key_t *key, value_t *value) /* {{{ */
665 {
666   value_list_t vl = VALUE_LIST_INIT;
667   char *host;
668
669   vl.values     = value;
670   vl.values_len = 1;
671
672   if ((db->host == NULL)
673       || (strcmp ("", db->host) == 0)
674       || (strcmp (CJ_DEFAULT_HOST, db->host) == 0))
675     host = hostname_g;
676   else
677     host = db->host;
678
679   if (key->instance == NULL)
680     ssnprintf (vl.type_instance, sizeof (vl.type_instance), "%s-%s",
681                db->state[db->depth-1].name, db->state[db->depth].name);
682   else
683     sstrncpy (vl.type_instance, key->instance, sizeof (vl.type_instance));
684
685   sstrncpy (vl.host, host, sizeof (vl.host));
686   sstrncpy (vl.plugin, "curl_json", sizeof (vl.plugin));
687   sstrncpy (vl.plugin_instance, db->instance, sizeof (vl.plugin_instance));
688   sstrncpy (vl.type, key->type, sizeof (vl.type));
689
690   plugin_dispatch_values (&vl);
691 } /* }}} int cj_submit */
692
693 static int cj_curl_perform (cj_t *db, CURL *curl) /* {{{ */
694 {
695   int status;
696   long rc;
697   char *url;
698   yajl_handle yprev = db->yajl;
699
700   db->yajl = yajl_alloc (&ycallbacks, NULL, NULL, (void *)db);
701   if (db->yajl == NULL)
702   {
703     ERROR ("curl_json plugin: yajl_alloc failed.");
704     db->yajl = yprev;
705     return (-1);
706   }
707
708   url = NULL;
709   curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &url);
710
711   status = curl_easy_perform (curl);
712   if (status != 0)
713   {
714     ERROR ("curl_json plugin: curl_easy_perform failed with status %i: %s (%s)",
715            status, db->curl_errbuf, (url != NULL) ? url : "<null>");
716     yajl_free (db->yajl);
717     db->yajl = yprev;
718     return (-1);
719   }
720
721   curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &rc);
722
723   /* The response code is zero if a non-HTTP transport was used. */
724   if ((rc != 0) && (rc != 200))
725   {
726     ERROR ("curl_json plugin: curl_easy_perform failed with "
727         "response code %ld (%s)", rc, url);
728     yajl_free (db->yajl);
729     db->yajl = yprev;
730     return (-1);
731   }
732
733   status = yajl_parse_complete (db->yajl);
734   if (status != yajl_status_ok)
735   {
736     unsigned char *errmsg;
737
738     errmsg = yajl_get_error (db->yajl, /* verbose = */ 0,
739         /* jsonText = */ NULL, /* jsonTextLen = */ 0);
740     ERROR ("curl_json plugin: yajl_parse_complete failed: %s",
741         (char *) errmsg);
742     yajl_free_error (db->yajl, errmsg);
743     yajl_free (db->yajl);
744     db->yajl = yprev;
745     return (-1);
746   }
747
748   yajl_free (db->yajl);
749   db->yajl = yprev;
750   return (0);
751 } /* }}} int cj_curl_perform */
752
753 static int cj_read (user_data_t *ud) /* {{{ */
754 {
755   cj_t *db;
756
757   if ((ud == NULL) || (ud->data == NULL))
758   {
759     ERROR ("curl_json plugin: cj_read: Invalid user data.");
760     return (-1);
761   }
762
763   db = (cj_t *) ud->data;
764
765   db->depth = 0;
766   memset (&db->state, 0, sizeof(db->state));
767   db->state[db->depth].tree = db->tree;
768   db->key = NULL;
769
770   return cj_curl_perform (db, db->curl);
771 } /* }}} int cj_read */
772
773 void module_register (void)
774 {
775   plugin_register_complex_config ("curl_json", cj_config);
776 } /* void module_register */
777
778 /* vim: set sw=2 sts=2 et fdm=marker : */