361e9e93c1155700c14c003ba6423e9341fdbee0
[collectd.git] / src / couchdb.c
1 /**
2  * collectd - src/couchdb.c
3  * Copyright (C) 2009       Doug MacEachern
4  * Copyright (C) 2006-2009  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 COUCHDB_DEFAULT_HOST "localhost"
34 #define COUCHDB_KEY_MAGIC 0x43484b59UL /* CHKY */
35 #define COUCHDB_IS_KEY(key) (key)->magic == COUCHDB_KEY_MAGIC
36 #define COUCHDB_ANY "*"
37 #define COUCH_MIN(x,y) ((x) < (y) ? (x) : (y))
38
39 struct couchdb_key_s;
40 typedef struct couchdb_key_s couchdb_key_t;
41 struct couchdb_key_s /* {{{ */
42 {
43   char *path;
44   char *type;
45   char *instance;
46   unsigned long magic;
47 };
48 /* }}} */
49
50 struct couchdb_s /* {{{ */
51 {
52   char *instance;
53   char *host;
54
55   char *url;
56   char *user;
57   char *pass;
58   char *credentials;
59   int   verify_peer;
60   int   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   couchdb_key_t *key;
69   int depth;
70   struct {
71     union {
72       c_avl_tree_t *tree;
73       couchdb_key_t *key;
74     };
75     char name[DATA_MAX_NAME_LEN];
76   } state[YAJL_MAX_DEPTH];
77 };
78 typedef struct couchdb_s couchdb_t; /* }}} */
79
80 static int couchdb_read (user_data_t *ud);
81 static int couchdb_curl_perform (couchdb_t *db, CURL *curl);
82 static void couchdb_submit (couchdb_t *db, couchdb_key_t *key, value_t *value);
83
84 static size_t couchdb_curl_callback (void *buf, /* {{{ */
85     size_t size, size_t nmemb, void *user_data)
86 {
87   couchdb_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   {
103     status = yajl_parse_complete(db->yajl);
104     return (len);
105   }
106   else if (status == yajl_status_insufficient_data)
107     return (len);
108
109   if (status != yajl_status_ok)
110   {
111     unsigned char *msg =
112       yajl_get_error(db->yajl, 1, (unsigned char *)buf, len);
113     ERROR ("couchdb plugin: yajl_parse failed: %s", msg);
114     yajl_free_error(db->yajl, msg);
115     return (0); /* abort write callback */
116   }
117
118   return (len);
119 } /* }}} size_t couchdb_curl_callback */
120
121 static int couchdb_get_type (couchdb_key_t *key)
122 {
123   const data_set_t *ds;
124
125   ds = plugin_get_ds (key->type);
126   if (ds == NULL)
127     return -1; /* let plugin_write do the complaining */
128   else
129     return ds->ds[0].type; /* XXX support ds->ds_len > 1 */
130 }
131
132 /* yajl callbacks */
133 static int couchcb_integer (void *ctx, long val)
134 {
135   couchdb_t *db = (couchdb_t *)ctx;
136   couchdb_key_t *key = db->state[db->depth].key;
137
138   if (key != NULL)
139   {
140     value_t vt;
141     int type;
142
143     type = couchdb_get_type (key);
144     if (type == DS_TYPE_COUNTER)
145       vt.counter = val;
146     else
147       vt.gauge = (double)val;
148
149     couchdb_submit (db, key, &vt);
150   }
151   return 1;
152 }
153
154 static int couchcb_double (void *ctx, double val)
155 {
156   couchdb_t *db = (couchdb_t *)ctx;
157   couchdb_key_t *key = db->state[db->depth].key;
158
159   if (key != NULL)
160   {
161     value_t vt;
162     int type;
163
164     type = couchdb_get_type (key);
165     if (type == DS_TYPE_GAUGE)
166       vt.gauge = val;
167     else
168       vt.counter = val;
169
170     couchdb_submit (db, key, &vt);
171   }
172   return 1;
173 }
174
175 static int couchcb_map_key (void *ctx, const unsigned char *val,
176                             unsigned int len)
177 {
178   couchdb_t *db = (couchdb_t *)ctx;
179   c_avl_tree_t *tree;
180
181   tree = db->state[db->depth-1].tree;
182
183   if (tree != NULL)
184   {
185     couchdb_key_t *value;
186     char *name;
187
188     name = db->state[db->depth].name;
189     len = COUCH_MIN(len, sizeof (db->state[db->depth].name)-1);
190     sstrncpy (name, (char *)val, len+1);
191
192     if (c_avl_get (tree, name, (void *) &value) == 0)
193       db->state[db->depth].key = value;
194     else if (c_avl_get (tree, COUCHDB_ANY, (void *) &value) == 0)
195       db->state[db->depth].key = value;
196     else
197       db->state[db->depth].key = NULL;
198   }
199
200   return 1;
201 }
202
203 static int couchcb_string (void *ctx, const unsigned char *val,
204                            unsigned int len)
205 {
206   couchdb_t *db = (couchdb_t *)ctx;
207   c_avl_tree_t *tree;
208   char *ptr;
209
210   if (db->depth != 1) /* e.g. _all_dbs */
211     return 1;
212
213   couchcb_map_key (ctx, val, len); /* same logic */
214
215   tree = db->state[db->depth].tree;
216
217   if ((tree != NULL) && (ptr = rindex (db->url, '/')))
218   {
219     char url[PATH_MAX];
220     CURL *curl;
221
222     /* url =~ s,[^/]+$,$name, */
223     len = (ptr - db->url) + 1;
224     ptr = url;
225     sstrncpy (ptr, db->url, sizeof (url));
226     sstrncpy (ptr + len, db->state[db->depth].name, sizeof (url) - len);
227
228     curl = curl_easy_duphandle (db->curl);
229     curl_easy_setopt (curl, CURLOPT_URL, url);
230     couchdb_curl_perform (db, curl);
231     curl_easy_cleanup (curl);
232   }
233   return 1;
234 }
235
236 static int couchcb_start (void *ctx)
237 {
238   couchdb_t *db = (couchdb_t *)ctx;
239   if (++db->depth >= YAJL_MAX_DEPTH)
240   {
241     ERROR ("couchdb plugin: %s depth exceeds max, aborting.", db->url);
242     return 0;
243   }
244   return 1;
245 }
246
247 static int couchcb_end (void *ctx)
248 {
249   couchdb_t *db = (couchdb_t *)ctx;
250   db->state[db->depth].tree = NULL;
251   --db->depth;
252   return 1;
253 }
254
255 static int couchcb_start_map (void *ctx)
256 {
257   return couchcb_start (ctx);
258 }
259
260 static int couchcb_end_map (void *ctx)
261 {
262   return couchcb_end (ctx);
263 }
264
265 static int couchcb_start_array (void * ctx)
266 {
267   return couchcb_start (ctx);
268 }
269
270 static int couchcb_end_array (void * ctx)
271 {
272   return couchcb_start (ctx);
273 }
274
275 static yajl_callbacks ycallbacks = {
276   NULL, /* null */
277   NULL, /* boolean */
278   couchcb_integer,
279   couchcb_double,
280   NULL, /* number */
281   couchcb_string,
282   couchcb_start_map,
283   couchcb_map_key,
284   couchcb_end_map,
285   couchcb_start_array,
286   couchcb_end_array
287 };
288
289 /* end yajl callbacks */
290
291 static void couchdb_key_free (couchdb_key_t *key) /* {{{ */
292 {
293   if (key == NULL)
294     return;
295
296   sfree (key->path);
297   sfree (key->type);
298   sfree (key->instance);
299
300   sfree (key);
301 } /* }}} void couchdb_key_free */
302
303 static void couchdb_tree_free (c_avl_tree_t *tree) /* {{{ */
304 {
305   char *name;
306   void *value;
307
308   while (c_avl_pick (tree, (void *) &name, (void *) &value) == 0)
309   {
310     couchdb_key_t *key = (couchdb_key_t *)value;
311
312     if (COUCHDB_IS_KEY(key))
313       couchdb_key_free (key);
314     else
315       couchdb_tree_free ((c_avl_tree_t *)value);
316
317     sfree (name);
318   }
319
320   c_avl_destroy (tree);
321 } /* }}} void couchdb_tree_free */
322
323 static void couchdb_free (void *arg) /* {{{ */
324 {
325   couchdb_t *db;
326
327   DEBUG ("couchdb plugin: couchdb_free (arg = %p);", arg);
328
329   db = (couchdb_t *) arg;
330
331   if (db == NULL)
332     return;
333
334   if (db->curl != NULL)
335     curl_easy_cleanup (db->curl);
336   db->curl = NULL;
337
338   if (db->tree != NULL)
339     couchdb_tree_free (db->tree);
340   db->tree = NULL;
341
342   sfree (db->instance);
343   sfree (db->host);
344
345   sfree (db->url);
346   sfree (db->user);
347   sfree (db->pass);
348   sfree (db->credentials);
349   sfree (db->cacert);
350
351   sfree (db);
352 } /* }}} void couchdb_free */
353
354 /* Configuration handling functions {{{ */
355
356 static int couchdb_config_add_string (const char *name, char **dest, /* {{{ */
357                                       oconfig_item_t *ci)
358 {
359   if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_STRING))
360   {
361     WARNING ("couchdb plugin: `%s' needs exactly one string argument.", name);
362     return (-1);
363   }
364
365   sfree (*dest);
366   *dest = strdup (ci->values[0].value.string);
367   if (*dest == NULL)
368     return (-1);
369
370   return (0);
371 } /* }}} int couchdb_config_add_string */
372
373 static int couchdb_config_set_boolean (const char *name, int *dest, /* {{{ */
374                                        oconfig_item_t *ci)
375 {
376   if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_BOOLEAN))
377   {
378     WARNING ("couchdb plugin: `%s' needs exactly one boolean argument.", name);
379     return (-1);
380   }
381
382   *dest = ci->values[0].value.boolean ? 1 : 0;
383
384   return (0);
385 } /* }}} int couchdb_config_set_boolean */
386
387 static c_avl_tree_t *couchdb_avl_create(void)
388 {
389   return c_avl_create ((int (*) (const void *, const void *)) strcmp);
390 }
391
392 static int couchdb_config_add_key (couchdb_t *db, /* {{{ */
393                                    oconfig_item_t *ci)
394 {
395   couchdb_key_t *key;
396   int status;
397   int i;
398
399   if ((ci->values_num != 1)
400       || (ci->values[0].type != OCONFIG_TYPE_STRING))
401   {
402     WARNING ("couchdb plugin: The `Key' block "
403              "needs exactly one string argument.");
404     return (-1);
405   }
406
407   key = (couchdb_key_t *) malloc (sizeof (*key));
408   if (key == NULL)
409   {
410     ERROR ("couchdb plugin: malloc failed.");
411     return (-1);
412   }
413   memset (key, 0, sizeof (*key));
414   key->magic = COUCHDB_KEY_MAGIC;
415
416   if (strcasecmp ("Key", ci->key) == 0)
417   {
418     status = couchdb_config_add_string ("Key", &key->path, ci);
419     if (status != 0)
420     {
421       sfree (key);
422       return (status);
423     }
424   }
425   else
426   {
427     ERROR ("couchdb plugin: couchdb_config: "
428            "Invalid key: %s", ci->key);
429     return (-1);
430   }
431
432   status = 0;
433   for (i = 0; i < ci->children_num; i++)
434   {
435     oconfig_item_t *child = ci->children + i;
436
437     if (strcasecmp ("Type", child->key) == 0)
438       status = couchdb_config_add_string ("Type", &key->type, child);
439     else if (strcasecmp ("Instance", child->key) == 0)
440       status = couchdb_config_add_string ("Instance", &key->instance, child);
441     else
442     {
443       WARNING ("couchdb plugin: Option `%s' not allowed here.", child->key);
444       status = -1;
445     }
446
447     if (status != 0)
448       break;
449   } /* for (i = 0; i < ci->children_num; i++) */
450
451   while (status == 0)
452   {
453     if (key->type == NULL)
454     {
455       WARNING ("couchdb plugin: `Type' missing in `Key' block.");
456       status = -1;
457     }
458
459     break;
460   } /* while (status == 0) */
461
462   /* store path in a tree that will match the json map structure, example:
463    * "httpd/requests/count",
464    * "httpd/requests/current" ->
465    * { "httpd": { "requests": { "count": $key, "current": $key } } }
466    */
467   if (status == 0)
468   {
469     char *ptr;
470     char *name;
471     char ent[PATH_MAX];
472     c_avl_tree_t *tree;
473
474     if (db->tree == NULL)
475       db->tree = couchdb_avl_create();
476
477     tree = db->tree;
478     name = key->path;
479     ptr = key->path;
480     if (*ptr == '/')
481       ++ptr;
482
483     name = ptr;
484     while (*ptr)
485     {
486       if (*ptr == '/')
487       {
488         c_avl_tree_t *value;
489         int len;
490
491         len = ptr-name;
492         if (len == 0)
493           break;
494         sstrncpy (ent, name, len+1);
495
496         if (c_avl_get (tree, ent, (void *) &value) != 0)
497         {
498           value = couchdb_avl_create ();
499           c_avl_insert (tree, strdup (ent), value);
500         }
501
502         tree = value;
503         name = ptr+1;
504       }
505       ++ptr;
506     }
507     if (*name)
508       c_avl_insert (tree, strdup(name), key);
509     else
510     {
511       ERROR ("couchdb plugin: invalid key: %s", key->path);
512       status = -1;
513     }
514   }
515
516   return (status);
517 } /* }}} int couchdb_config_add_key */
518
519 static int couchdb_init_curl (couchdb_t *db) /* {{{ */
520 {
521   db->curl = curl_easy_init ();
522   if (db->curl == NULL)
523   {
524     ERROR ("couchdb plugin: curl_easy_init failed.");
525     return (-1);
526   }
527
528   curl_easy_setopt (db->curl, CURLOPT_WRITEFUNCTION, couchdb_curl_callback);
529   curl_easy_setopt (db->curl, CURLOPT_WRITEDATA, db);
530   curl_easy_setopt (db->curl, CURLOPT_USERAGENT,
531                     PACKAGE_NAME"/"PACKAGE_VERSION);
532   curl_easy_setopt (db->curl, CURLOPT_ERRORBUFFER, db->curl_errbuf);
533   curl_easy_setopt (db->curl, CURLOPT_URL, db->url);
534
535   if (db->user != NULL)
536   {
537     size_t credentials_size;
538
539     credentials_size = strlen (db->user) + 2;
540     if (db->pass != NULL)
541       credentials_size += strlen (db->pass);
542
543     db->credentials = (char *) malloc (credentials_size);
544     if (db->credentials == NULL)
545     {
546       ERROR ("couchdb plugin: malloc failed.");
547       return (-1);
548     }
549
550     ssnprintf (db->credentials, credentials_size, "%s:%s",
551                db->user, (db->pass == NULL) ? "" : db->pass);
552     curl_easy_setopt (db->curl, CURLOPT_USERPWD, db->credentials);
553   }
554
555   curl_easy_setopt (db->curl, CURLOPT_SSL_VERIFYPEER, db->verify_peer);
556   curl_easy_setopt (db->curl, CURLOPT_SSL_VERIFYHOST,
557                     db->verify_host ? 2 : 0);
558   if (db->cacert != NULL)
559     curl_easy_setopt (db->curl, CURLOPT_CAINFO, db->cacert);
560
561   return (0);
562 } /* }}} int couchdb_init_curl */
563
564 static int couchdb_config_add_url (oconfig_item_t *ci) /* {{{ */
565 {
566   couchdb_t *db;
567   int status = 0;
568   int i;
569
570   if ((ci->values_num != 1)
571       || (ci->values[0].type != OCONFIG_TYPE_STRING))
572   {
573     WARNING ("couchdb plugin: The `URL' block "
574              "needs exactly one string argument.");
575     return (-1);
576   }
577
578   db = (couchdb_t *) malloc (sizeof (*db));
579   if (db == NULL)
580   {
581     ERROR ("couchdb plugin: malloc failed.");
582     return (-1);
583   }
584   memset (db, 0, sizeof (*db));
585
586   if (strcasecmp ("URL", ci->key) == 0)
587   {
588     status = couchdb_config_add_string ("URL", &db->url, ci);
589     if (status != 0)
590     {
591       sfree (db);
592       return (status);
593     }
594   }
595   else
596   {
597     ERROR ("couchdb plugin: couchdb_config: "
598            "Invalid key: %s", ci->key);
599     return (-1);
600   }
601
602   /* Fill the `couchdb_t' structure.. */
603   for (i = 0; i < ci->children_num; i++)
604   {
605     oconfig_item_t *child = ci->children + i;
606
607     if (strcasecmp ("Instance", child->key) == 0)
608       status = couchdb_config_add_string ("Instance", &db->instance, child);
609     else if (strcasecmp ("Host", child->key) == 0)
610       status = couchdb_config_add_string ("Host", &db->host, child);
611     else if (strcasecmp ("User", child->key) == 0)
612       status = couchdb_config_add_string ("User", &db->user, child);
613     else if (strcasecmp ("Password", child->key) == 0)
614       status = couchdb_config_add_string ("Password", &db->pass, child);
615     else if (strcasecmp ("VerifyPeer", child->key) == 0)
616       status = couchdb_config_set_boolean ("VerifyPeer", &db->verify_peer, child);
617     else if (strcasecmp ("VerifyHost", child->key) == 0)
618       status = couchdb_config_set_boolean ("VerifyHost", &db->verify_host, child);
619     else if (strcasecmp ("CACert", child->key) == 0)
620       status = couchdb_config_add_string ("CACert", &db->cacert, child);
621     else if (strcasecmp ("Key", child->key) == 0)
622       status = couchdb_config_add_key (db, child);
623     else
624     {
625       WARNING ("couchdb plugin: Option `%s' not allowed here.", child->key);
626       status = -1;
627     }
628
629     if (status != 0)
630       break;
631   }
632
633   if (status == 0)
634   {
635     if (db->tree == NULL)
636     {
637       WARNING ("couchdb plugin: No (valid) `Key' block "
638                "within `URL' block `%s'.", db->url);
639       status = -1;
640     }
641     if (status == 0)
642       status = couchdb_init_curl (db);
643   }
644
645   /* If all went well, register this database for reading */
646   if (status == 0)
647   {
648     user_data_t ud;
649     char cb_name[DATA_MAX_NAME_LEN];
650
651     if (db->instance == NULL)
652       db->instance = strdup("default");
653
654     DEBUG ("couchdb plugin: Registering new read callback: %s",
655            db->instance);
656
657     memset (&ud, 0, sizeof (ud));
658     ud.data = (void *) db;
659     ud.free_func = couchdb_free;
660
661     ssnprintf (cb_name, sizeof (cb_name), "couchdb-%s-%s",
662                db->instance, db->url);
663
664     plugin_register_complex_read (cb_name, couchdb_read,
665                                   /* interval = */ NULL, &ud);
666   }
667   else
668   {
669     couchdb_free (db);
670     return (-1);
671   }
672
673   return (0);
674 }
675  /* }}} int couchdb_config_add_database */
676
677 static int couchdb_config (oconfig_item_t *ci) /* {{{ */
678 {
679   int success;
680   int errors;
681   int status;
682   int i;
683
684   success = 0;
685   errors = 0;
686
687   for (i = 0; i < ci->children_num; i++)
688   {
689     oconfig_item_t *child = ci->children + i;
690
691     if (strcasecmp ("URL", child->key) == 0)
692     {
693       status = couchdb_config_add_url (child);
694       if (status == 0)
695         success++;
696       else
697         errors++;
698     }
699     else
700     {
701       WARNING ("couchdb plugin: Option `%s' not allowed here.", child->key);
702       errors++;
703     }
704   }
705
706   if ((success == 0) && (errors > 0))
707   {
708     ERROR ("couchdb plugin: All statements failed.");
709     return (-1);
710   }
711
712   return (0);
713 } /* }}} int couchdb_config */
714
715 /* }}} End of configuration handling functions */
716
717 static void couchdb_submit (couchdb_t *db, couchdb_key_t *key, value_t *value) /* {{{ */
718 {
719   value_list_t vl = VALUE_LIST_INIT;
720   char *host;
721
722   vl.values     = value;
723   vl.values_len = 1;
724
725   if ((db->host == NULL)
726       || (strcmp ("", db->host) == 0)
727       || (strcmp (COUCHDB_DEFAULT_HOST, db->host) == 0))
728     host = hostname_g;
729   else
730     host = db->host;
731
732   if (key->instance == NULL)
733     ssnprintf (vl.type_instance, sizeof (vl.type_instance), "%s-%s",
734                db->state[db->depth-1].name, db->state[db->depth].name);
735   else
736     sstrncpy (vl.type_instance, key->instance, sizeof (vl.type_instance));
737
738   sstrncpy (vl.host, host, sizeof (vl.host));
739   sstrncpy (vl.plugin, "couchdb", sizeof (vl.plugin));
740   sstrncpy (vl.plugin_instance, db->instance, sizeof (vl.plugin_instance));
741   sstrncpy (vl.type, key->type, sizeof (vl.type));
742
743   plugin_dispatch_values (&vl);
744 } /* }}} int couchdb_submit */
745
746 static int couchdb_curl_perform (couchdb_t *db, CURL *curl) /* {{{ */
747 {
748   int status;
749   long rc;
750   char *url;
751   yajl_handle yprev = db->yajl;
752
753   db->yajl = yajl_alloc (&ycallbacks, NULL, NULL, (void *)db);
754   if (db->yajl == NULL)
755   {
756     ERROR ("couchdb plugin: yajl_alloc failed.");
757     return (-1);
758   }
759
760   status = curl_easy_perform (curl);
761
762   yajl_free (db->yajl);
763   db->yajl = yprev;
764
765   curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &url);
766   curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &rc);
767
768   if (rc != 200)
769   {
770     ERROR ("couchdb plugin: curl_easy_perform failed with response code %ld (%s)",
771            rc, url);
772     return (-1);
773   }
774
775   if (status != 0)
776   {
777     ERROR ("couchdb plugin: curl_easy_perform failed with status %i: %s (%s)",
778            status, db->curl_errbuf, url);
779     return (-1);
780   }
781
782   return (0);
783 } /* }}} int couchdb_curl_perform */
784
785 static int couchdb_read (user_data_t *ud) /* {{{ */
786 {
787   couchdb_t *db;
788
789   if ((ud == NULL) || (ud->data == NULL))
790   {
791     ERROR ("couchdb plugin: couchdb_read: Invalid user data.");
792     return (-1);
793   }
794
795   db = (couchdb_t *) ud->data;
796
797   db->depth = 0;
798   memset (&db->state, 0, sizeof(db->state));
799   db->state[db->depth].tree = db->tree;
800   db->key = NULL;
801
802   return couchdb_curl_perform (db, db->curl);
803 } /* }}} int couchdb_read */
804
805 void module_register (void)
806 {
807   plugin_register_complex_config ("couchdb", couchdb_config);
808 } /* void module_register */