mysql plugin: Added support for master/slave statistics.
[collectd.git] / src / mysql.c
1 /**
2  * collectd - src/mysql.c
3  * Copyright (C) 2006-2009  Florian octo Forster
4  * Copyright (C) 2009  Sebastian tokkee Harl
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  *   Florian octo Forster <octo at verplant.org>
21  *   Sebastian tokkee Harl <sh at tokkee.org>
22  **/
23
24 #include "collectd.h"
25 #include "common.h"
26 #include "plugin.h"
27 #include "configfile.h"
28
29 #ifdef HAVE_MYSQL_H
30 #include <mysql.h>
31 #elif defined(HAVE_MYSQL_MYSQL_H)
32 #include <mysql/mysql.h>
33 #endif
34
35 /* TODO: Understand `Select_*' and possibly do that stuff as well.. */
36
37 struct mysql_database_s /* {{{ */
38 {
39         /* instance == NULL  =>  legacy mode */
40         char *instance;
41         char *host;
42         char *user;
43         char *pass;
44         char *database;
45         char *socket;
46         int   port;
47
48         int   master_stats;
49         int   slave_stats;
50
51         MYSQL *con;
52         int    state;
53 };
54 typedef struct mysql_database_s mysql_database_t; /* }}} */
55
56 static int mysql_read (user_data_t *ud);
57
58 static void mysql_database_free (void *arg) /* {{{ */
59 {
60         mysql_database_t *db;
61
62         DEBUG ("mysql plugin: mysql_database_free (arg = %p);", arg);
63
64         db = (mysql_database_t *) arg;
65
66         if (db == NULL)
67                 return;
68
69         if (db->con != NULL)
70                 mysql_close (db->con);
71
72         sfree (db->host);
73         sfree (db->user);
74         sfree (db->pass);
75         sfree (db->socket);
76         sfree (db->instance);
77         sfree (db->database);
78         sfree (db);
79 } /* }}} void mysql_database_free */
80
81 /* Configuration handling functions {{{
82  *
83  * <Plugin mysql>
84  *   <Database "plugin_instance1">
85  *     Host "localhost"
86  *     Port 22000
87  *     ...
88  *   </Database>
89  * </Plugin>
90  */
91
92 static int mysql_config_set_string (char **ret_string, /* {{{ */
93                                     oconfig_item_t *ci)
94 {
95         char *string;
96
97         if ((ci->values_num != 1)
98             || (ci->values[0].type != OCONFIG_TYPE_STRING))
99         {
100                 WARNING ("mysql plugin: The `%s' config option "
101                          "needs exactly one string argument.", ci->key);
102                 return (-1);
103         }
104
105         string = strdup (ci->values[0].value.string);
106         if (string == NULL)
107         {
108                 ERROR ("mysql plugin: strdup failed.");
109                 return (-1);
110         }
111
112         if (*ret_string != NULL)
113                 free (*ret_string);
114         *ret_string = string;
115
116         return (0);
117 } /* }}} int mysql_config_set_string */
118
119 static int mysql_config_set_int (int *ret_int, /* {{{ */
120                                  oconfig_item_t *ci)
121 {
122         if ((ci->values_num != 1)
123             || (ci->values[0].type != OCONFIG_TYPE_NUMBER))
124         {
125                 WARNING ("mysql plugin: The `%s' config option "
126                          "needs exactly one string argument.", ci->key);
127                 return (-1);
128         }
129
130         *ret_int = ci->values[0].value.number;
131
132         return (0);
133 } /* }}} int mysql_config_set_int */
134
135 static int mysql_config_set_boolean (int *ret_boolean, /* {{{ */
136                                 oconfig_item_t *ci)
137 {
138         int status = 0;
139
140         if (ci->values_num != 1)
141                 status = -1;
142
143         if (status == 0)
144         {
145                 if (ci->values[0].type == OCONFIG_TYPE_BOOLEAN)
146                         *ret_boolean = ci->values[0].value.boolean;
147                 else if (ci->values[0].type == OCONFIG_TYPE_STRING)
148                 {
149                         if (IS_TRUE (ci->values[0].value.string))
150                                 *ret_boolean = 1;
151                         else if (IS_FALSE (ci->values[0].value.string))
152                                 *ret_boolean = 0;
153                         else
154                                 status = -1;
155                 }
156                 else
157                         status = -1;
158         }
159
160         if (status != 0)
161         {
162                 WARNING ("mysql plugin: The `%s' config option "
163                         "needs exactly one boolean argument.", ci->key);
164                 return (-1);
165         }
166         return (0);
167 } /* }}} mysql_config_set_boolean */
168
169 static int mysql_config (oconfig_item_t *ci) /* {{{ */
170 {
171         mysql_database_t *db;
172         int plugin_block;
173         int status = 0;
174         int i;
175
176         if ((ci->values_num != 1)
177             || (ci->values[0].type != OCONFIG_TYPE_STRING))
178         {
179                 WARNING ("mysql plugin: The `Database' block "
180                          "needs exactly one string argument.");
181                 return (-1);
182         }
183
184         db = (mysql_database_t *) malloc (sizeof (*db));
185         if (db == NULL)
186         {
187                 ERROR ("mysql plugin: malloc failed.");
188                 return (-1);
189         }
190         memset (db, 0, sizeof (*db));
191
192         /* initialize all the pointers */
193         db->host     = NULL;
194         db->user     = NULL;
195         db->pass     = NULL;
196         db->database = NULL;
197         db->socket   = NULL;
198         db->con      = NULL;
199
200         plugin_block = 1;
201         if (strcasecmp ("Plugin", ci->key) == 0)
202         {
203                 db->instance = NULL;
204         }
205         else if (strcasecmp ("Database", ci->key) == 0)
206         {
207                 plugin_block = 0;
208                 status = mysql_config_set_string (&db->instance, ci);
209                 if (status != 0)
210                 {
211                         sfree (db);
212                         return (status);
213                 }
214                 assert (db->instance != NULL);
215                 db->database = strdup (db->instance);
216         }
217         else
218         {
219                 ERROR ("mysql plugin: mysql_config: "
220                                 "Invalid key: %s", ci->key);
221                 return (-1);
222         }
223
224         /* Fill the `mysql_database_t' structure.. */
225         for (i = 0; i < ci->children_num; i++)
226         {
227                 oconfig_item_t *child = ci->children + i;
228
229                 if (strcasecmp ("Host", child->key) == 0)
230                         status = mysql_config_set_string (&db->host, child);
231                 else if (strcasecmp ("User", child->key) == 0)
232                         status = mysql_config_set_string (&db->user, child);
233                 else if (strcasecmp ("Password", child->key) == 0)
234                         status = mysql_config_set_string (&db->pass, child);
235                 else if (strcasecmp ("Port", child->key) == 0)
236                         status = mysql_config_set_int (&db->port, child);
237                 else if (strcasecmp ("Socket", child->key) == 0)
238                         status = mysql_config_set_string (&db->socket, child);
239                 /* Check if we're currently handling the `Plugin' block. If so,
240                  * handle `Database' _blocks_, too. */
241                 else if ((plugin_block != 0)
242                                 && (strcasecmp ("Database", child->key) == 0)
243                                 && (child->children != NULL))
244                 {
245                         /* If `plugin_block > 1', there has been at least one
246                          * `Database' block */
247                         plugin_block++;
248                         status = mysql_config (child);
249                 }
250                 /* Now handle ordinary `Database' options (without children) */
251                 else if ((strcasecmp ("Database", child->key) == 0)
252                                 && (child->children == NULL))
253                         status = mysql_config_set_string (&db->database, child);
254                 else if (strcasecmp ("MasterStats", child->key) == 0)
255                         status = mysql_config_set_boolean (&db->master_stats, child);
256                 else if (strcasecmp ("SlaveStats", child->key) == 0)
257                         status = mysql_config_set_boolean (&db->slave_stats, child);
258                 else
259                 {
260                         WARNING ("mysql plugin: Option `%s' not allowed here.", child->key);
261                         status = -1;
262                 }
263
264                 if (status != 0)
265                         break;
266         }
267
268         /* Check if there were any `Database' blocks. */
269         if (plugin_block > 1)
270         {
271                 /* There were connection blocks. Don't use any legacy stuff. */
272                 if ((db->host != NULL)
273                         || (db->user != NULL)
274                         || (db->pass != NULL)
275                         || (db->database != NULL)
276                         || (db->socket != NULL)
277                         || (db->port != 0))
278                 {
279                         WARNING ("mysql plugin: At least one <Database> "
280                                         "block has been found. The legacy "
281                                         "configuration will be ignored.");
282                 }
283                 mysql_database_free (db);
284                 return (0);
285         }
286         else if (plugin_block != 0)
287         {
288                 WARNING ("mysql plugin: You're using the legacy "
289                                 "configuration options. Please consider "
290                                 "updating your configuration!");
291         }
292
293         /* Check that all necessary options have been given. */
294         while (status == 0)
295         {
296                 /* Zero is allowed and automatically handled by
297                  * `mysql_real_connect'. */
298                 if ((db->port < 0) || (db->port > 65535))
299                 {
300                         ERROR ("mysql plugin: Database %s: Port number out "
301                                         "of range: %i",
302                                         (db->instance != NULL)
303                                         ? db->instance
304                                         : "<legacy>",
305                                         db->port);
306                         status = -1;
307                 }
308                 if (db->database == NULL)
309                 {
310                         ERROR ("mysql plugin: No `Database' configured");
311                         status = -1;
312                 }
313                 break;
314         } /* while (status == 0) */
315
316         /* If all went well, register this database for reading */
317         if (status == 0)
318         {
319                 user_data_t ud;
320                 char cb_name[DATA_MAX_NAME_LEN];
321
322                 DEBUG ("mysql plugin: Registering new read callback: %s", db->database);
323
324                 memset (&ud, 0, sizeof (ud));
325                 ud.data = (void *) db;
326                 ud.free_func = mysql_database_free;
327
328                 if (db->database != NULL)
329                         ssnprintf (cb_name, sizeof (cb_name), "mysql-%s",
330                                         db->database);
331                 else
332                         sstrncpy (cb_name, "mysql", sizeof (cb_name));
333
334                 plugin_register_complex_read (cb_name, mysql_read,
335                                               /* interval = */ NULL, &ud);
336         }
337         else
338         {
339                 mysql_database_free (db);
340                 return (-1);
341         }
342
343         return (0);
344 } /* }}} int mysql_config */
345
346 /* }}} End of configuration handling functions */
347
348 static MYSQL *getconnection (mysql_database_t *db)
349 {
350         if (db->state != 0)
351         {
352                 int err;
353                 if ((err = mysql_ping (db->con)) != 0)
354                 {
355                         WARNING ("mysql_ping failed: %s", mysql_error (db->con));
356                         db->state = 0;
357                 }
358                 else
359                 {
360                         db->state = 1;
361                         return (db->con);
362                 }
363         }
364
365         if ((db->con = mysql_init (db->con)) == NULL)
366         {
367                 ERROR ("mysql_init failed: %s", mysql_error (db->con));
368                 db->state = 0;
369                 return (NULL);
370         }
371
372         if (mysql_real_connect (db->con, db->host, db->user, db->pass,
373                                 db->database, db->port, db->socket, 0) == NULL)
374         {
375                 ERROR ("mysql_real_connect failed: %s", mysql_error (db->con));
376                 db->state = 0;
377                 return (NULL);
378         }
379         else
380         {
381                 db->state = 1;
382                 return (db->con);
383         }
384 } /* static MYSQL *getconnection (mysql_database_t *db) */
385
386 static void set_host (mysql_database_t *db, value_list_t *vl)
387 {
388         /* XXX legacy mode - use hostname_g */
389         if (db->instance == NULL)
390                 sstrncpy (vl->host, hostname_g, sizeof (vl->host));
391         else
392         {
393                 if ((db->host == NULL)
394                                 || (strcmp ("", db->host) == 0)
395                                 || (strcmp ("localhost", db->host) == 0))
396                         sstrncpy (vl->host, hostname_g, sizeof (vl->host));
397                 else
398                         sstrncpy (vl->host, db->host, sizeof (vl->host));
399         }
400 }
401
402 static void set_plugin_instance (mysql_database_t *db, value_list_t *vl)
403 {
404         /* XXX legacy mode - no plugin_instance */
405         if (db->instance == NULL)
406                 sstrncpy (vl->plugin_instance, "",
407                                 sizeof (vl->plugin_instance));
408         else
409                 sstrncpy (vl->plugin_instance, db->instance,
410                                 sizeof (vl->plugin_instance));
411 }
412
413 static void submit (const char *type, const char *type_instance,
414                 value_t *values, size_t values_len, mysql_database_t *db)
415 {
416         value_list_t vl = VALUE_LIST_INIT;
417
418         vl.values     = values;
419         vl.values_len = values_len;
420
421         set_host (db, &vl);
422
423         sstrncpy (vl.plugin, "mysql", sizeof (vl.plugin));
424         set_plugin_instance (db, &vl);
425
426         sstrncpy (vl.type, type, sizeof (vl.type));
427         if (type_instance != NULL)
428                 sstrncpy (vl.type_instance, type_instance, sizeof (vl.type_instance));
429
430         plugin_dispatch_values (&vl);
431 } /* submit */
432
433 static void counter_submit (const char *type, const char *type_instance,
434                 counter_t value, mysql_database_t *db)
435 {
436         value_t values[1];
437
438         values[0].counter = value;
439         submit (type, type_instance, values, STATIC_ARRAY_SIZE (values), db);
440 } /* void counter_submit */
441
442 static void gauge_submit (const char *type, const char *type_instance,
443                 gauge_t value, mysql_database_t *db)
444 {
445         value_t values[1];
446
447         values[0].gauge = value;
448         submit (type, type_instance, values, STATIC_ARRAY_SIZE (values), db);
449 } /* void gauge_submit */
450
451 static void qcache_submit (counter_t hits, counter_t inserts,
452                 counter_t not_cached, counter_t lowmem_prunes,
453                 gauge_t queries_in_cache, mysql_database_t *db)
454 {
455         value_t values[5];
456
457         values[0].counter = hits;
458         values[1].counter = inserts;
459         values[2].counter = not_cached;
460         values[3].counter = lowmem_prunes;
461         values[4].gauge   = queries_in_cache;
462
463         submit ("mysql_qcache", NULL, values, STATIC_ARRAY_SIZE (values), db);
464 } /* void qcache_submit */
465
466 static void threads_submit (gauge_t running, gauge_t connected, gauge_t cached,
467                 counter_t created, mysql_database_t *db)
468 {
469         value_t values[4];
470
471         values[0].gauge   = running;
472         values[1].gauge   = connected;
473         values[2].gauge   = cached;
474         values[3].counter = created;
475
476         submit ("mysql_threads", NULL, values, STATIC_ARRAY_SIZE (values), db);
477 } /* void threads_submit */
478
479 static void traffic_submit (counter_t rx, counter_t tx, mysql_database_t *db)
480 {
481         value_t values[2];
482
483         values[0].counter = rx;
484         values[1].counter = tx;
485
486         submit ("mysql_octets", NULL, values, STATIC_ARRAY_SIZE (values), db);
487 } /* void traffic_submit */
488
489 static MYSQL_RES *exec_query (MYSQL *con, const char *query)
490 {
491         MYSQL_RES *res;
492
493         int query_len = strlen (query);
494
495         if (mysql_real_query (con, query, query_len))
496         {
497                 ERROR ("mysql plugin: Failed to execute query: %s",
498                                 mysql_error (con));
499                 INFO ("mysql plugin: SQL query was: %s", query);
500                 return (NULL);
501         }
502
503         res = mysql_store_result (con);
504         if (res == NULL)
505         {
506                 ERROR ("mysql plugin: Failed to store query result: %s",
507                                 mysql_error (con));
508                 INFO ("mysql plugin: SQL query was: %s", query);
509                 return (NULL);
510         }
511
512         return (res);
513 } /* exec_query */
514
515 static int mysql_read_master_stats (mysql_database_t *db, MYSQL *con)
516 {
517         MYSQL_RES *res;
518         MYSQL_ROW  row;
519
520         char *query;
521         int   field_num;
522         unsigned long long position;
523
524         query = "SHOW MASTER STATUS";
525
526         res = exec_query (con, query);
527         if (res == NULL)
528                 return (-1);
529
530         row = mysql_fetch_row (res);
531         if (row == NULL)
532         {
533                 ERROR ("mysql plugin: Failed to get master statistics: "
534                                 "`%s' did not return any rows.", query);
535                 return (-1);
536         }
537
538         field_num = mysql_num_fields (res);
539         if (field_num < 2)
540         {
541                 ERROR ("mysql plugin: Failed to get master statistics: "
542                                 "`%s' returned less than two columns.", query);
543                 return (-1);
544         }
545
546         position = atoll (row[1]);
547         counter_submit ("mysql_log_position", "master-bin", position, db);
548
549         row = mysql_fetch_row (res);
550         if (row != NULL)
551                 WARNING ("mysql plugin: `%s' returned more than one row - "
552                                 "ignoring further results.", query);
553
554         mysql_free_result (res);
555
556         return (0);
557 } /* mysql_read_master_stats */
558
559 static int mysql_read_slave_stats (mysql_database_t *db, MYSQL *con)
560 {
561         MYSQL_RES *res;
562         MYSQL_ROW  row;
563
564         char *query;
565         int   field_num;
566
567         /* WTF? libmysqlclient does not seem to provide any means to
568          * translate a column name to a column index ... :-/ */
569         const int READ_MASTER_LOG_POS_IDX   = 6;
570         const int EXEC_MASTER_LOG_POS_IDX   = 21;
571         const int SECONDS_BEHIND_MASTER_IDX = 32;
572
573         unsigned long long counter;
574         double gauge;
575
576         query = "SHOW SLAVE STATUS";
577
578         res = exec_query (con, query);
579         if (res == NULL)
580                 return (-1);
581
582         row = mysql_fetch_row (res);
583         if (row == NULL)
584         {
585                 ERROR ("mysql plugin: Failed to get slave statistics: "
586                                 "`%s' did not return any rows.", query);
587                 return (-1);
588         }
589
590         field_num = mysql_num_fields (res);
591         if (field_num < 33)
592         {
593                 ERROR ("mysql plugin: Failed to get slave statistics: "
594                                 "`%s' returned less than 33 columns.", query);
595                 return (-1);
596         }
597
598         counter = atoll (row[READ_MASTER_LOG_POS_IDX]);
599         counter_submit ("mysql_log_position", "slave-read", counter, db);
600
601         counter = atoll (row[EXEC_MASTER_LOG_POS_IDX]);
602         counter_submit ("mysql_log_position", "slave-exec", counter, db);
603
604         if (row[SECONDS_BEHIND_MASTER_IDX] != NULL)
605         {
606                 gauge = atof (row[SECONDS_BEHIND_MASTER_IDX]);
607                 gauge_submit ("time_offset", NULL, gauge, db);
608         }
609
610         row = mysql_fetch_row (res);
611         if (row != NULL)
612                 WARNING ("mysql plugin: `%s' returned more than one row - "
613                                 "ignoring further results.", query);
614
615         mysql_free_result (res);
616
617         return (0);
618 } /* mysql_read_slave_stats */
619
620 static int mysql_read (user_data_t *ud)
621 {
622         mysql_database_t *db;
623         MYSQL     *con;
624         MYSQL_RES *res;
625         MYSQL_ROW  row;
626         char      *query;
627         int        field_num;
628
629         unsigned long long qcache_hits          = 0ULL;
630         unsigned long long qcache_inserts       = 0ULL;
631         unsigned long long qcache_not_cached    = 0ULL;
632         unsigned long long qcache_lowmem_prunes = 0ULL;
633         int qcache_queries_in_cache = -1;
634
635         int threads_running   = -1;
636         int threads_connected = -1;
637         int threads_cached    = -1;
638         unsigned long long threads_created = 0ULL;
639
640         unsigned long long traffic_incoming = 0ULL;
641         unsigned long long traffic_outgoing = 0ULL;
642
643         if ((ud == NULL) || (ud->data == NULL))
644         {
645                 ERROR ("mysql plugin: mysql_database_read: Invalid user data.");
646                 return (-1);
647         }
648
649         db = (mysql_database_t *) ud->data;
650
651         /* An error message will have been printed in this case */
652         if ((con = getconnection (db)) == NULL)
653                 return (-1);
654
655         query = "SHOW STATUS";
656         if (mysql_get_server_version (con) >= 50002)
657                 query = "SHOW GLOBAL STATUS";
658
659         res = exec_query (con, query);
660         if (res == NULL)
661                 return (-1);
662
663         field_num = mysql_num_fields (res);
664         while ((row = mysql_fetch_row (res)))
665         {
666                 char *key;
667                 unsigned long long val;
668
669                 key = row[0];
670                 val = atoll (row[1]);
671
672                 if (strncmp (key, "Com_", 4) == 0)
673                 {
674                         if (val == 0ULL)
675                                 continue;
676
677                         /* Ignore `prepared statements' */
678                         if (strncmp (key, "Com_stmt_", 9) != 0)
679                                 counter_submit ("mysql_commands", key + 4, val, db);
680                 }
681                 else if (strncmp (key, "Handler_", 8) == 0)
682                 {
683                         if (val == 0ULL)
684                                 continue;
685
686                         counter_submit ("mysql_handler", key + 8, val, db);
687                 }
688                 else if (strncmp (key, "Qcache_", 7) == 0)
689                 {
690                         if (strcmp (key, "Qcache_hits") == 0)
691                                 qcache_hits = val;
692                         else if (strcmp (key, "Qcache_inserts") == 0)
693                                 qcache_inserts = val;
694                         else if (strcmp (key, "Qcache_not_cached") == 0)
695                                 qcache_not_cached = val;
696                         else if (strcmp (key, "Qcache_lowmem_prunes") == 0)
697                                 qcache_lowmem_prunes = val;
698                         else if (strcmp (key, "Qcache_queries_in_cache") == 0)
699                                 qcache_queries_in_cache = (int) val;
700                 }
701                 else if (strncmp (key, "Bytes_", 6) == 0)
702                 {
703                         if (strcmp (key, "Bytes_received") == 0)
704                                 traffic_incoming += val;
705                         else if (strcmp (key, "Bytes_sent") == 0)
706                                 traffic_outgoing += val;
707                 }
708                 else if (strncmp (key, "Threads_", 8) == 0)
709                 {
710                         if (strcmp (key, "Threads_running") == 0)
711                                 threads_running = (int) val;
712                         else if (strcmp (key, "Threads_connected") == 0)
713                                 threads_connected = (int) val;
714                         else if (strcmp (key, "Threads_cached") == 0)
715                                 threads_cached = (int) val;
716                         else if (strcmp (key, "Threads_created") == 0)
717                                 threads_created = val;
718                 }
719         }
720         mysql_free_result (res); res = NULL;
721
722         if ((qcache_hits != 0ULL)
723                         || (qcache_inserts != 0ULL)
724                         || (qcache_not_cached != 0ULL)
725                         || (qcache_lowmem_prunes != 0ULL))
726                 qcache_submit (qcache_hits, qcache_inserts, qcache_not_cached,
727                                qcache_lowmem_prunes, qcache_queries_in_cache, db);
728
729         if (threads_created != 0ULL)
730                 threads_submit (threads_running, threads_connected,
731                                 threads_cached, threads_created, db);
732
733         traffic_submit  (traffic_incoming, traffic_outgoing, db);
734
735         if (db->master_stats)
736                 mysql_read_master_stats (db, con);
737
738         if (db->slave_stats)
739                 mysql_read_slave_stats (db, con);
740
741         return (0);
742 } /* int mysql_read */
743
744 void module_register (void)
745 {
746         plugin_register_complex_config ("mysql", mysql_config);
747 } /* void module_register */