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