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