23234b831772b0a5c6bec38496bb2fa659ba7e3f
[collectd.git] / src / redis.c
1 /**
2  * collectd - src/redis.c, based on src/memcached.c
3  * Copyright (C) 2010       Andrés J. Díaz <ajdiaz@connectical.com>
4  *
5  * This program is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License as published by the
7  * Free Software Foundation; either version 2 of the License, or (at your
8  * option) any later version.
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  *   Andrés J. Díaz <ajdiaz@connectical.com>
21  **/
22
23 #include "collectd.h"
24
25 #include "common.h"
26 #include "plugin.h"
27
28 #include <hiredis/hiredis.h>
29 #include <sys/time.h>
30
31 #define REDIS_DEF_HOST "localhost"
32 #define REDIS_DEF_PASSWD ""
33 #define REDIS_DEF_PORT 6379
34 #define REDIS_DEF_TIMEOUT_SEC 2
35 #define REDIS_DEF_DB_COUNT 256
36 #define MAX_REDIS_VAL_SIZE 256
37 #define MAX_REDIS_QUERY 2048
38
39 /* Redis plugin configuration example:
40  *
41  * <Plugin redis>
42  *   <Node "mynode">
43  *     Host "localhost"
44  *     Port "6379"
45  *     Timeout 2
46  *     Password "foobar"
47  *   </Node>
48  * </Plugin>
49  */
50
51 struct redis_query_s;
52 typedef struct redis_query_s redis_query_t;
53 struct redis_query_s {
54   char query[MAX_REDIS_QUERY];
55   char type[DATA_MAX_NAME_LEN];
56   char instance[DATA_MAX_NAME_LEN];
57   int database;
58
59   redis_query_t *next;
60 };
61
62 struct redis_node_s;
63 typedef struct redis_node_s redis_node_t;
64 struct redis_node_s {
65   char *name;
66   char *host;
67   char *passwd;
68   int port;
69   struct timeval timeout;
70   bool report_command_stats;
71   bool report_cpu_usage;
72   redisContext *redisContext;
73   redis_query_t *queries;
74
75   redis_node_t *next;
76 };
77
78 static bool redis_have_instances;
79 static int redis_read(user_data_t *user_data);
80
81 static void redis_node_free(void *arg) {
82   redis_node_t *rn = arg;
83   if (rn == NULL)
84     return;
85
86   redis_query_t *rq = rn->queries;
87   while (rq != NULL) {
88     redis_query_t *next = rq->next;
89     sfree(rq);
90     rq = next;
91   }
92
93   redisFree(rn->redisContext);
94   sfree(rn->name);
95   sfree(rn->host);
96   sfree(rn->passwd);
97   sfree(rn);
98 } /* void redis_node_free */
99
100 static int redis_node_add(redis_node_t *rn) /* {{{ */
101 {
102   DEBUG("redis plugin: Adding node \"%s\".", rn->name);
103
104   /* Disable automatic generation of default instance in the init callback. */
105   redis_have_instances = true;
106
107   char cb_name[sizeof("redis/") + DATA_MAX_NAME_LEN];
108   snprintf(cb_name, sizeof(cb_name), "redis/%s", rn->name);
109
110   return plugin_register_complex_read(
111       /* group = */ "redis",
112       /* name      = */ cb_name,
113       /* callback  = */ redis_read,
114       /* interval  = */ 0,
115       &(user_data_t){
116           .data = rn, .free_func = redis_node_free,
117       });
118 } /* }}} */
119
120 static redis_query_t *redis_config_query(oconfig_item_t *ci) /* {{{ */
121 {
122   redis_query_t *rq;
123   int status;
124
125   rq = calloc(1, sizeof(*rq));
126   if (rq == NULL) {
127     ERROR("redis plugin: calloc failed adding redis_query.");
128     return NULL;
129   }
130   status = cf_util_get_string_buffer(ci, rq->query, sizeof(rq->query));
131   if (status != 0)
132     goto err;
133
134   /*
135    * Default to a gauge type.
136    */
137   (void)strncpy(rq->type, "gauge", sizeof(rq->type));
138   (void)sstrncpy(rq->instance, rq->query, sizeof(rq->instance));
139   replace_special(rq->instance, sizeof(rq->instance));
140
141   rq->database = 0;
142
143   for (int i = 0; i < ci->children_num; i++) {
144     oconfig_item_t *option = ci->children + i;
145
146     if (strcasecmp("Type", option->key) == 0) {
147       status = cf_util_get_string_buffer(option, rq->type, sizeof(rq->type));
148     } else if (strcasecmp("Instance", option->key) == 0) {
149       status =
150           cf_util_get_string_buffer(option, rq->instance, sizeof(rq->instance));
151     } else if (strcasecmp("Database", option->key) == 0) {
152       status = cf_util_get_int(option, &rq->database);
153       if (rq->database < 0) {
154         WARNING("redis plugin: The \"Database\" option must be positive "
155                 "integer or zero");
156         status = -1;
157       }
158     } else {
159       WARNING("redis plugin: unknown configuration option: %s", option->key);
160       status = -1;
161     }
162     if (status != 0)
163       goto err;
164   }
165   return rq;
166 err:
167   free(rq);
168   return NULL;
169 } /* }}} */
170
171 static int redis_config_node(oconfig_item_t *ci) /* {{{ */
172 {
173   redis_node_t *rn = calloc(1, sizeof(*rn));
174   if (rn == NULL) {
175     ERROR("redis plugin: calloc failed adding node.");
176     return ENOMEM;
177   }
178
179   rn->port = REDIS_DEF_PORT;
180   rn->timeout.tv_sec = REDIS_DEF_TIMEOUT_SEC;
181   rn->report_cpu_usage = true;
182
183   rn->host = strdup(REDIS_DEF_HOST);
184   if (rn->host == NULL) {
185     ERROR("redis plugin: strdup failed adding node.");
186     sfree(rn);
187     return ENOMEM;
188   }
189
190   int status = cf_util_get_string(ci, &rn->name);
191   if (status != 0) {
192     sfree(rn->host);
193     sfree(rn);
194     return status;
195   }
196
197   for (int i = 0; i < ci->children_num; i++) {
198     oconfig_item_t *option = ci->children + i;
199
200     if (strcasecmp("Host", option->key) == 0)
201       status = cf_util_get_string(option, &rn->host);
202     else if (strcasecmp("Port", option->key) == 0) {
203       status = cf_util_get_port_number(option);
204       if (status > 0) {
205         rn->port = status;
206         status = 0;
207       }
208     } else if (strcasecmp("Query", option->key) == 0) {
209       redis_query_t *rq = redis_config_query(option);
210       if (rq == NULL) {
211         status = 1;
212       } else {
213         rq->next = rn->queries;
214         rn->queries = rq;
215       }
216     } else if (strcasecmp("Timeout", option->key) == 0) {
217       int timeout;
218       status = cf_util_get_int(option, &timeout);
219       if (status == 0) {
220         rn->timeout.tv_usec = timeout * 1000;
221         rn->timeout.tv_sec = rn->timeout.tv_usec / 1000000L;
222         rn->timeout.tv_usec %= 1000000L;
223       }
224     } else if (strcasecmp("Password", option->key) == 0)
225       status = cf_util_get_string(option, &rn->passwd);
226     else if (strcasecmp("ReportCommandStats", option->key) == 0)
227       status = cf_util_get_boolean(option, &rn->report_command_stats);
228     else if (strcasecmp("ReportCpuUsage", option->key) == 0)
229       status = cf_util_get_boolean(option, &rn->report_cpu_usage);
230     else
231       WARNING("redis plugin: Option `%s' not allowed inside a `Node' "
232               "block. I'll ignore this option.",
233               option->key);
234
235     if (status != 0)
236       break;
237   }
238
239   if (status != 0) {
240     redis_node_free(rn);
241     return status;
242   }
243
244   return redis_node_add(rn);
245 } /* }}} int redis_config_node */
246
247 static int redis_config(oconfig_item_t *ci) /* {{{ */
248 {
249   for (int i = 0; i < ci->children_num; i++) {
250     oconfig_item_t *option = ci->children + i;
251
252     if (strcasecmp("Node", option->key) == 0)
253       redis_config_node(option);
254     else
255       WARNING("redis plugin: Option `%s' not allowed in redis"
256               " configuration. It will be ignored.",
257               option->key);
258   }
259
260   return 0;
261 } /* }}} */
262
263 __attribute__((nonnull(2))) static void
264 redis_submit(const char *plugin_instance, const char *type,
265              const char *type_instance, value_t value) /* {{{ */
266 {
267   value_list_t vl = VALUE_LIST_INIT;
268
269   vl.values = &value;
270   vl.values_len = 1;
271   sstrncpy(vl.plugin, "redis", sizeof(vl.plugin));
272   if (plugin_instance != NULL)
273     sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance));
274   sstrncpy(vl.type, type, sizeof(vl.type));
275   if (type_instance != NULL)
276     sstrncpy(vl.type_instance, type_instance, sizeof(vl.type_instance));
277
278   plugin_dispatch_values(&vl);
279 } /* }}} */
280
281 __attribute__((nonnull(2))) static void
282 redis_submit2(const char *plugin_instance, const char *type,
283               const char *type_instance, value_t value0,
284               value_t value1) /* {{{ */
285 {
286   value_list_t vl = VALUE_LIST_INIT;
287   value_t values[] = {value0, value1};
288
289   vl.values = values;
290   vl.values_len = STATIC_ARRAY_SIZE(values);
291
292   sstrncpy(vl.plugin, "redis", sizeof(vl.plugin));
293   sstrncpy(vl.type, type, sizeof(vl.type));
294
295   if (plugin_instance != NULL)
296     sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance));
297
298   if (type_instance != NULL)
299     sstrncpy(vl.type_instance, type_instance, sizeof(vl.type_instance));
300
301   plugin_dispatch_values(&vl);
302 } /* }}} */
303
304 static int redis_init(void) /* {{{ */
305 {
306   if (redis_have_instances)
307     return 0;
308
309   redis_node_t *rn = calloc(1, sizeof(*rn));
310   if (rn == NULL)
311     return ENOMEM;
312
313   rn->port = REDIS_DEF_PORT;
314   rn->timeout.tv_sec = REDIS_DEF_TIMEOUT_SEC;
315
316   rn->name = strdup("default");
317   rn->host = strdup(REDIS_DEF_HOST);
318
319   if (rn->name == NULL || rn->host == NULL)
320     return ENOMEM;
321
322   return redis_node_add(rn);
323 } /* }}} int redis_init */
324
325 static void *c_redisCommand(redis_node_t *rn, const char *format, ...) {
326   redisContext *c = rn->redisContext;
327
328   if (c == NULL)
329     return NULL;
330
331   va_list ap;
332   va_start(ap, format);
333   void *reply = redisvCommand(c, format, ap);
334   va_end(ap);
335
336   if (reply == NULL) {
337     ERROR("redis plugin: Connection error: %s", c->errstr);
338     redisFree(rn->redisContext);
339     rn->redisContext = NULL;
340   }
341
342   return reply;
343 } /* void c_redisCommand */
344
345 static int redis_get_info_value(char const *info_line, char const *field_name,
346                                 int ds_type, value_t *val) {
347   char *str = strstr(info_line, field_name);
348   static char buf[MAX_REDIS_VAL_SIZE];
349   if (str) {
350     int i;
351
352     str += strlen(field_name) + 1; /* also skip the ':' */
353     for (i = 0; (*str && (isdigit((unsigned char)*str) || *str == '.'));
354          i++, str++)
355       buf[i] = *str;
356     buf[i] = '\0';
357
358     if (parse_value(buf, val, ds_type) == -1) {
359       WARNING("redis plugin: Unable to parse field `%s'.", field_name);
360       return -1;
361     }
362
363     return 0;
364   }
365   return -1;
366 } /* int redis_get_info_value */
367
368 static int redis_handle_info(char *node, char const *info_line,
369                              char const *type, char const *type_instance,
370                              char const *field_name, int ds_type) /* {{{ */
371 {
372   value_t val;
373   if (redis_get_info_value(info_line, field_name, ds_type, &val) != 0)
374     return -1;
375
376   redis_submit(node, type, type_instance, val);
377   return 0;
378 } /* }}} int redis_handle_info */
379
380 static int redis_handle_query(redis_node_t *rn, redis_query_t *rq) /* {{{ */
381 {
382   redisReply *rr;
383   const data_set_t *ds;
384   value_t val;
385
386   ds = plugin_get_ds(rq->type);
387   if (!ds) {
388     ERROR("redis plugin: DS type `%s' not defined.", rq->type);
389     return -1;
390   }
391
392   if (ds->ds_num != 1) {
393     ERROR("redis plugin: DS type `%s' has too many datasources. This is not "
394           "supported currently.",
395           rq->type);
396     return -1;
397   }
398
399   if ((rr = c_redisCommand(rn, "SELECT %d", rq->database)) == NULL) {
400     WARNING("redis plugin: unable to switch to database `%d' on node `%s'.",
401             rq->database, rn->name);
402     return -1;
403   }
404
405   if ((rr = c_redisCommand(rn, rq->query)) == NULL) {
406     WARNING("redis plugin: unable to carry out query `%s'.", rq->query);
407     return -1;
408   }
409
410   switch (rr->type) {
411   case REDIS_REPLY_INTEGER:
412     switch (ds->ds[0].type) {
413     case DS_TYPE_COUNTER:
414       val.counter = (counter_t)rr->integer;
415       break;
416     case DS_TYPE_GAUGE:
417       val.gauge = (gauge_t)rr->integer;
418       break;
419     case DS_TYPE_DERIVE:
420       val.gauge = (derive_t)rr->integer;
421       break;
422     case DS_TYPE_ABSOLUTE:
423       val.gauge = (absolute_t)rr->integer;
424       break;
425     }
426     break;
427   case REDIS_REPLY_STRING:
428     if (parse_value(rr->str, &val, ds->ds[0].type) == -1) {
429       WARNING("redis plugin: Query `%s': Unable to parse value.", rq->query);
430       freeReplyObject(rr);
431       return -1;
432     }
433     break;
434   case REDIS_REPLY_ERROR:
435     WARNING("redis plugin: Query `%s' failed: %s.", rq->query, rr->str);
436     freeReplyObject(rr);
437     return -1;
438   case REDIS_REPLY_ARRAY:
439     WARNING("redis plugin: Query `%s' should return string or integer. Arrays "
440             "are not supported.",
441             rq->query);
442     freeReplyObject(rr);
443     return -1;
444   default:
445     WARNING("redis plugin: Query `%s': Cannot coerce redis type (%i).",
446             rq->query, rr->type);
447     freeReplyObject(rr);
448     return -1;
449   }
450
451   redis_submit(rn->name, rq->type,
452                (strlen(rq->instance) > 0) ? rq->instance : NULL, val);
453   freeReplyObject(rr);
454   return 0;
455 } /* }}} int redis_handle_query */
456
457 static int redis_db_stats(const char *node, char const *info_line) /* {{{ */
458 {
459   /* redis_db_stats parses and dispatches Redis database statistics,
460    * currently the number of keys for each database.
461    * info_line needs to have the following format:
462    *   db0:keys=4,expires=0,avg_ttl=0
463    */
464
465   for (int db = 0; db < REDIS_DEF_DB_COUNT; db++) {
466     static char buf[MAX_REDIS_VAL_SIZE];
467     static char field_name[12];
468     static char db_id[4];
469     value_t val;
470     char *str;
471     int i;
472
473     snprintf(field_name, sizeof(field_name), "db%d:keys=", db);
474
475     str = strstr(info_line, field_name);
476     if (!str)
477       continue;
478
479     str += strlen(field_name);
480     for (i = 0; (*str && isdigit((int)*str)); i++, str++)
481       buf[i] = *str;
482     buf[i] = '\0';
483
484     if (parse_value(buf, &val, DS_TYPE_GAUGE) != 0) {
485       WARNING("redis plugin: Unable to parse field `%s'.", field_name);
486       return -1;
487     }
488
489     snprintf(db_id, sizeof(db_id), "%d", db);
490     redis_submit(node, "records", db_id, val);
491   }
492   return 0;
493
494 } /* }}} int redis_db_stats */
495
496 static void redis_cpu_usage(const char *node, char const *info_line) {
497   while (42) {
498     value_t rusage_user;
499     value_t rusage_syst;
500
501     if (redis_get_info_value(info_line, "used_cpu_user", DS_TYPE_GAUGE,
502                              &rusage_user) != 0)
503       break;
504
505     if (redis_get_info_value(info_line, "used_cpu_sys", DS_TYPE_GAUGE,
506                              &rusage_syst) != 0)
507       break;
508
509     redis_submit2(node, "ps_cputime", "daemon",
510                   (value_t){.derive = rusage_user.gauge * 1000000},
511                   (value_t){.derive = rusage_syst.gauge * 1000000});
512     break;
513   }
514
515   while (42) {
516     value_t rusage_user;
517     value_t rusage_syst;
518
519     if (redis_get_info_value(info_line, "used_cpu_user_children", DS_TYPE_GAUGE,
520                              &rusage_user) != 0)
521       break;
522
523     if (redis_get_info_value(info_line, "used_cpu_sys_children", DS_TYPE_GAUGE,
524                              &rusage_syst) != 0)
525       break;
526
527     redis_submit2(node, "ps_cputime", "children",
528                   (value_t){.derive = rusage_user.gauge * 1000000},
529                   (value_t){.derive = rusage_syst.gauge * 1000000});
530     break;
531   }
532 } /* void redis_cpu_usage */
533
534 static void redis_check_connection(redis_node_t *rn) {
535   if (rn->redisContext)
536     return;
537
538   redisContext *rh = redisConnectWithTimeout(rn->host, rn->port, rn->timeout);
539
540   if (rh == NULL) {
541     ERROR("redis plugin: can't allocate redis context");
542     return;
543   }
544   if (rh->err) {
545     ERROR("redis plugin: unable to connect to node `%s' (%s:%d): %s.", rn->name,
546           rn->host, rn->port, rh->errstr);
547     redisFree(rh);
548     return;
549   }
550
551   rn->redisContext = rh;
552
553   if (rn->passwd) {
554     redisReply *rr;
555
556     DEBUG("redis plugin: authenticating node `%s' passwd(%s).", rn->name,
557           rn->passwd);
558
559     if ((rr = c_redisCommand(rn, "AUTH %s", rn->passwd)) == NULL) {
560       WARNING("redis plugin: unable to authenticate on node `%s'.", rn->name);
561       return;
562     }
563
564     if (rr->type != REDIS_REPLY_STATUS) {
565       WARNING("redis plugin: invalid authentication on node `%s'.", rn->name);
566       freeReplyObject(rr);
567       redisFree(rn->redisContext);
568       rn->redisContext = NULL;
569       return;
570     }
571
572     freeReplyObject(rr);
573   }
574   return;
575 } /* void redis_check_connection */
576
577 static void redis_read_server_info(redis_node_t *rn) {
578   redisReply *rr;
579
580   if ((rr = c_redisCommand(rn, "INFO")) == NULL) {
581     WARNING("redis plugin: unable to get INFO from node `%s'.", rn->name);
582     return;
583   }
584
585   redis_handle_info(rn->name, rr->str, "uptime", NULL, "uptime_in_seconds",
586                     DS_TYPE_GAUGE);
587   redis_handle_info(rn->name, rr->str, "current_connections", "clients",
588                     "connected_clients", DS_TYPE_GAUGE);
589   redis_handle_info(rn->name, rr->str, "blocked_clients", NULL,
590                     "blocked_clients", DS_TYPE_GAUGE);
591   redis_handle_info(rn->name, rr->str, "memory", NULL, "used_memory",
592                     DS_TYPE_GAUGE);
593   redis_handle_info(rn->name, rr->str, "memory_lua", NULL, "used_memory_lua",
594                     DS_TYPE_GAUGE);
595   /* changes_since_last_save: Deprecated in redis version 2.6 and above */
596   redis_handle_info(rn->name, rr->str, "volatile_changes", NULL,
597                     "changes_since_last_save", DS_TYPE_GAUGE);
598   redis_handle_info(rn->name, rr->str, "total_connections", NULL,
599                     "total_connections_received", DS_TYPE_DERIVE);
600   redis_handle_info(rn->name, rr->str, "total_operations", NULL,
601                     "total_commands_processed", DS_TYPE_DERIVE);
602   redis_handle_info(rn->name, rr->str, "operations_per_second", NULL,
603                     "instantaneous_ops_per_sec", DS_TYPE_GAUGE);
604   redis_handle_info(rn->name, rr->str, "expired_keys", NULL, "expired_keys",
605                     DS_TYPE_DERIVE);
606   redis_handle_info(rn->name, rr->str, "evicted_keys", NULL, "evicted_keys",
607                     DS_TYPE_DERIVE);
608   redis_handle_info(rn->name, rr->str, "pubsub", "channels", "pubsub_channels",
609                     DS_TYPE_GAUGE);
610   redis_handle_info(rn->name, rr->str, "pubsub", "patterns", "pubsub_patterns",
611                     DS_TYPE_GAUGE);
612   redis_handle_info(rn->name, rr->str, "current_connections", "slaves",
613                     "connected_slaves", DS_TYPE_GAUGE);
614   redis_handle_info(rn->name, rr->str, "cache_result", "hits", "keyspace_hits",
615                     DS_TYPE_DERIVE);
616   redis_handle_info(rn->name, rr->str, "cache_result", "misses",
617                     "keyspace_misses", DS_TYPE_DERIVE);
618   redis_handle_info(rn->name, rr->str, "total_bytes", "input",
619                     "total_net_input_bytes", DS_TYPE_DERIVE);
620   redis_handle_info(rn->name, rr->str, "total_bytes", "output",
621                     "total_net_output_bytes", DS_TYPE_DERIVE);
622
623   redis_db_stats(rn->name, rr->str);
624
625   if (rn->report_cpu_usage)
626     redis_cpu_usage(rn->name, rr->str);
627
628   freeReplyObject(rr);
629 } /* void redis_read_server_info */
630
631 static void redis_read_command_stats(redis_node_t *rn) {
632   redisReply *rr;
633
634   if ((rr = c_redisCommand(rn, "INFO commandstats")) == NULL) {
635     WARNING("redis plugin: node `%s': unable to get `INFO commandstats'.",
636             rn->name);
637     return;
638   }
639
640   if (rr->type != REDIS_REPLY_STRING) {
641     WARNING("redis plugin: node `%s' `INFO commandstats' returned unsupported "
642             "redis type %i.",
643             rn->name, rr->type);
644     freeReplyObject(rr);
645     return;
646   }
647
648   char *command;
649   char *line;
650   char *ptr = rr->str;
651   char *saveptr = NULL;
652   while ((line = strtok_r(ptr, "\n\r", &saveptr)) != NULL) {
653     ptr = NULL;
654
655     if (line[0] == '#')
656       continue;
657
658     /* command name */
659     if (strstr(line, "cmdstat_") != line) {
660       ERROR("redis plugin: not found 'cmdstat_' prefix in line '%s'", line);
661       continue;
662     }
663
664     char *values = strstr(line, ":");
665     if (values == NULL) {
666       ERROR("redis plugin: not found ':' separator in line '%s'", line);
667       continue;
668     }
669
670     /* Null-terminate command token */
671     values[0] = '\0';
672     command = line + strlen("cmdstat_");
673     values++;
674
675     /* parse values */
676     /* cmdstat_publish:calls=20795774,usec=111039258,usec_per_call=5.34 */
677     char *field;
678     char *saveptr_field = NULL;
679     while ((field = strtok_r(values, "=", &saveptr_field)) != NULL) {
680       values = NULL;
681
682       const char *type;
683       /* only these are supported */
684       if (strcmp(field, "calls") == 0)
685         type = "commands";
686       else if (strcmp(field, "usec") == 0)
687         type = "redis_command_cputime";
688       else
689         continue;
690
691       if ((field = strtok_r(NULL, ",", &saveptr_field)) == NULL)
692         continue;
693
694       char *endptr = NULL;
695       errno = 0;
696       derive_t value = strtoll(field, &endptr, 0);
697
698       if ((endptr == field) || (errno != 0))
699         continue;
700
701       redis_submit(rn->name, type, command, (value_t){.derive = value});
702     }
703   }
704   freeReplyObject(rr);
705 } /* void redis_read_command_stats */
706
707 static int redis_read(user_data_t *user_data) /* {{{ */
708 {
709   redis_node_t *rn = user_data->data;
710
711   DEBUG("redis plugin: querying info from node `%s' (%s:%d).", rn->name,
712         rn->host, rn->port);
713
714   redis_check_connection(rn);
715
716   if (!rn->redisContext) /* no connection */
717     return -1;
718
719   redis_read_server_info(rn);
720
721   if (!rn->redisContext) /* connection lost */
722     return -1;
723
724   if (rn->report_command_stats) {
725     redis_read_command_stats(rn);
726
727     if (!rn->redisContext) /* connection lost */
728       return -1;
729   }
730
731   for (redis_query_t *rq = rn->queries; rq != NULL; rq = rq->next) {
732     redis_handle_query(rn, rq);
733     if (!rn->redisContext) /* connection lost */
734       return -1;
735   }
736
737   return 0;
738 }
739 /* }}} */
740
741 void module_register(void) /* {{{ */
742 {
743   plugin_register_complex_config("redis", redis_config);
744   plugin_register_init("redis", redis_init);
745 }
746 /* }}} */