80fc7671c32939d19ace90e93e3ab15538b3992d
[collectd.git] / src / rrdtool.c
1 /**
2  * collectd - src/rrdtool.c
3  * Copyright (C) 2006  Florian octo Forster
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; only version 2 of the License is applicable.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
17  *
18  * Authors:
19  *   Florian octo Forster <octo at verplant.org>
20  **/
21
22 #include "collectd.h"
23 #include "plugin.h"
24 #include "common.h"
25 #include "utils_avltree.h"
26 #include "utils_debug.h"
27
28 /*
29  * This weird macro cascade forces the glibc to define `NAN'. I don't know
30  * another way to solve this, so more intelligent solutions are welcome. -octo
31  */
32 #ifndef __USE_ISOC99
33 # define DISABLE__USE_ISOC99 1
34 # define __USE_ISOC99 1
35 #endif
36 #include <math.h>
37 #ifdef DISABLE__USE_ISOC99
38 # undef DISABLE__USE_ISOC99
39 # undef __USE_ISOC99
40 #endif
41
42 /*
43  * Private types
44  */
45 struct rrd_cache_s
46 {
47         int    values_num;
48         char **values;
49         time_t first_value;
50 };
51 typedef struct rrd_cache_s rrd_cache_t;
52
53 /*
54  * Private variables
55  */
56 static int rra_timespans[] =
57 {
58         3600,
59         86400,
60         604800,
61         2678400,
62         31622400
63 };
64 static int rra_timespans_num = STATIC_ARRAY_SIZE (rra_timespans);
65
66 static char *rra_types[] =
67 {
68         "AVERAGE",
69         "MIN",
70         "MAX"
71 };
72 static int rra_types_num = STATIC_ARRAY_SIZE (rra_types);
73
74 static const char *config_keys[] =
75 {
76         "CacheTimeout",
77         "CacheFlush",
78         "DataDir",
79         "StepSize",
80         "HeartBeat",
81         "RRARows",
82         "XFF"
83 };
84 static int config_keys_num = STATIC_ARRAY_SIZE (config_keys);
85
86 static char   *datadir   = NULL;
87 static int     stepsize  = 0;
88 static int     heartbeat = 0;
89 static int     rrarows   = 1200;
90 static double  xff       = 0.1;
91
92 static int         cache_timeout = 0;
93 static int         cache_flush_timeout = 0;
94 static time_t      cache_flush_last;
95 static avl_tree_t *cache = NULL;
96
97 /* * * * * * * * * *
98  * WARNING:  Magic *
99  * * * * * * * * * */
100 static int rra_get (char ***ret)
101 {
102         static char **rra_def = NULL;
103         static int rra_num = 0;
104
105         int rra_max = rra_timespans_num * rra_types_num;
106
107         int span;
108
109         int cdp_num;
110         int cdp_len;
111         int i, j;
112
113         char buffer[64];
114
115         if ((rra_num != 0) && (rra_def != NULL))
116         {
117                 *ret = rra_def;
118                 return (rra_num);
119         }
120
121         if ((rra_def = (char **) malloc ((rra_max + 1) * sizeof (char *))) == NULL)
122                 return (-1);
123         memset (rra_def, '\0', (rra_max + 1) * sizeof (char *));
124
125         if ((stepsize <= 0) || (rrarows <= 0))
126         {
127                 *ret = NULL;
128                 return (-1);
129         }
130
131         cdp_len = 0;
132         for (i = 0; i < rra_timespans_num; i++)
133         {
134                 span = rra_timespans[i];
135
136                 if ((span / stepsize) < rrarows)
137                         continue;
138
139                 if (cdp_len == 0)
140                         cdp_len = 1;
141                 else
142                         cdp_len = (int) floor (((double) span)
143                                         / ((double) (rrarows * stepsize)));
144
145                 cdp_num = (int) ceil (((double) span)
146                                 / ((double) (cdp_len * stepsize)));
147
148                 for (j = 0; j < rra_types_num; j++)
149                 {
150                         if (rra_num >= rra_max)
151                                 break;
152
153                         if (snprintf (buffer, sizeof (buffer), "RRA:%s:%3.1f:%u:%u",
154                                                 rra_types[j], xff,
155                                                 cdp_len, cdp_num) >= sizeof (buffer))
156                         {
157                                 syslog (LOG_ERR, "rra_get: Buffer would have been truncated.");
158                                 continue;
159                         }
160
161                         rra_def[rra_num++] = sstrdup (buffer);
162                 }
163         }
164
165 #if COLLECT_DEBUG
166         DBG ("rra_num = %i", rra_num);
167         for (i = 0; i < rra_num; i++)
168                 DBG ("  %s", rra_def[i]);
169 #endif
170
171         *ret = rra_def;
172         return (rra_num);
173 }
174
175 static void ds_free (int ds_num, char **ds_def)
176 {
177         int i;
178
179         for (i = 0; i < ds_num; i++)
180                 if (ds_def[i] != NULL)
181                         free (ds_def[i]);
182         free (ds_def);
183 }
184
185 static int ds_get (char ***ret, const data_set_t *ds)
186 {
187         char **ds_def;
188         int ds_num;
189
190         char min[32];
191         char max[32];
192         char buffer[128];
193
194         DBG ("ds->ds_num = %i", ds->ds_num);
195
196         ds_def = (char **) malloc (ds->ds_num * sizeof (char *));
197         if (ds_def == NULL)
198         {
199                 syslog (LOG_ERR, "rrdtool plugin: malloc failed: %s",
200                                 strerror (errno));
201                 return (-1);
202         }
203         memset (ds_def, '\0', ds->ds_num * sizeof (char *));
204
205         for (ds_num = 0; ds_num < ds->ds_num; ds_num++)
206         {
207                 data_source_t *d = ds->ds + ds_num;
208                 char *type;
209                 int status;
210
211                 ds_def[ds_num] = NULL;
212
213                 if (d->type == DS_TYPE_COUNTER)
214                         type = "COUNTER";
215                 else if (d->type == DS_TYPE_GAUGE)
216                         type = "GAUGE";
217                 else
218                 {
219                         syslog (LOG_ERR, "rrdtool plugin: Unknown DS type: %i",
220                                         d->type);
221                         break;
222                 }
223
224                 if (d->min == NAN)
225                 {
226                         strcpy (min, "U");
227                 }
228                 else
229                 {
230                         snprintf (min, sizeof (min), "%lf", d->min);
231                         min[sizeof (min) - 1] = '\0';
232                 }
233
234                 if (d->max == NAN)
235                 {
236                         strcpy (max, "U");
237                 }
238                 else
239                 {
240                         snprintf (max, sizeof (max), "%lf", d->max);
241                         max[sizeof (max) - 1] = '\0';
242                 }
243
244                 status = snprintf (buffer, sizeof (buffer),
245                                 "DS:%s:%s:%i:%s:%s",
246                                 d->name, type, heartbeat,
247                                 min, max);
248                 if ((status < 1) || (status >= sizeof (buffer)))
249                         break;
250
251                 ds_def[ds_num] = sstrdup (buffer);
252         } /* for ds_num = 0 .. ds->ds_num */
253
254 #if COLLECT_DEBUG
255 {
256         int i;
257         DBG ("ds_num = %i", ds_num);
258         for (i = 0; i < ds_num; i++)
259                 DBG ("  %s", ds_def[i]);
260 }
261 #endif
262
263         if (ds_num != ds->ds_num)
264         {
265                 ds_free (ds_num, ds_def);
266                 return (-1);
267         }
268
269         *ret = ds_def;
270         return (ds_num);
271 }
272
273 static int rrd_create_file (char *filename, const data_set_t *ds)
274 {
275         char **argv;
276         int argc;
277         char **rra_def;
278         int rra_num;
279         char **ds_def;
280         int ds_num;
281         int i, j;
282         char stepsize_str[16];
283         int status = 0;
284
285         if (check_create_dir (filename))
286                 return (-1);
287
288         if ((rra_num = rra_get (&rra_def)) < 1)
289         {
290                 syslog (LOG_ERR, "rrd_create_file failed: Could not calculate RRAs");
291                 return (-1);
292         }
293
294         if ((ds_num = ds_get (&ds_def, ds)) < 1)
295         {
296                 syslog (LOG_ERR, "rrd_create_file failed: Could not calculate DSes");
297                 return (-1);
298         }
299
300         argc = ds_num + rra_num + 4;
301
302         if ((argv = (char **) malloc (sizeof (char *) * (argc + 1))) == NULL)
303         {
304                 syslog (LOG_ERR, "rrd_create failed: %s", strerror (errno));
305                 return (-1);
306         }
307
308         status = snprintf (stepsize_str, sizeof (stepsize_str),
309                         "%i", stepsize);
310         if ((status < 1) || (status >= sizeof (stepsize_str)))
311         {
312                 syslog (LOG_ERR, "rrdtool plugin: snprintf failed.");
313                 return (-1);
314         }
315
316         argv[0] = "create";
317         argv[1] = filename;
318         argv[2] = "-s";
319         argv[3] = stepsize_str;
320
321         j = 4;
322         for (i = 0; i < ds_num; i++)
323                 argv[j++] = ds_def[i];
324         for (i = 0; i < rra_num; i++)
325                 argv[j++] = rra_def[i];
326         argv[j] = NULL;
327
328         optind = 0; /* bug in librrd? */
329         rrd_clear_error ();
330         if (rrd_create (argc, argv) == -1)
331         {
332                 syslog (LOG_ERR, "rrd_create failed: %s: %s", filename, rrd_get_error ());
333                 status = -1;
334         }
335
336         free (argv);
337         ds_free (ds_num, ds_def);
338
339         return (status);
340 }
341
342 static int value_list_to_string (char *buffer, int buffer_len,
343                 const data_set_t *ds, const value_list_t *vl)
344 {
345         int offset;
346         int status;
347         int i;
348
349         memset (buffer, '\0', sizeof (buffer_len));
350
351         status = snprintf (buffer, buffer_len, "%u", (unsigned int) vl->time);
352         if ((status < 1) || (status >= buffer_len))
353                 return (-1);
354         offset = status;
355
356         for (i = 0; i < ds->ds_num; i++)
357         {
358                 if ((ds->ds[i].type != DS_TYPE_COUNTER)
359                                 && (ds->ds[i].type != DS_TYPE_GAUGE))
360                         return (-1);
361
362                 if (ds->ds[i].type == DS_TYPE_COUNTER)
363                         status = snprintf (buffer + offset, buffer_len - offset,
364                                         ":%llu", vl->values[i].counter);
365                 else
366                         status = snprintf (buffer + offset, buffer_len - offset,
367                                         ":%lf", vl->values[i].gauge);
368
369                 if ((status < 1) || (status >= (buffer_len - offset)))
370                         return (-1);
371
372                 offset += status;
373         } /* for ds->ds_num */
374
375         return (0);
376 } /* int value_list_to_string */
377
378 static int value_list_to_filename (char *buffer, int buffer_len,
379                 const data_set_t *ds, const value_list_t *vl)
380 {
381         int offset = 0;
382         int status;
383
384         if (datadir != NULL)
385         {
386                 status = snprintf (buffer + offset, buffer_len - offset,
387                                 "%s/", datadir);
388                 if ((status < 1) || (status >= buffer_len - offset))
389                         return (-1);
390                 offset += status;
391         }
392
393         status = snprintf (buffer + offset, buffer_len - offset,
394                         "%s/", vl->host);
395         if ((status < 1) || (status >= buffer_len - offset))
396                 return (-1);
397         offset += status;
398
399         if (strlen (vl->plugin_instance) > 0)
400                 status = snprintf (buffer + offset, buffer_len - offset,
401                                 "%s-%s/", vl->plugin, vl->plugin_instance);
402         else
403                 status = snprintf (buffer + offset, buffer_len - offset,
404                                 "%s/", vl->plugin);
405         if ((status < 1) || (status >= buffer_len - offset))
406                 return (-1);
407         offset += status;
408
409         if (strlen (vl->type_instance) > 0)
410                 status = snprintf (buffer + offset, buffer_len - offset,
411                                 "%s-%s.rrd", ds->type, vl->type_instance);
412         else
413                 status = snprintf (buffer + offset, buffer_len - offset,
414                                 "%s.rrd", ds->type);
415         if ((status < 1) || (status >= buffer_len - offset))
416                 return (-1);
417         offset += status;
418
419         return (0);
420 } /* int value_list_to_filename */
421
422 static rrd_cache_t *rrd_cache_insert (const char *filename,
423                 const char *value)
424 {
425         rrd_cache_t *rc = NULL;
426         int new_rc = 0;
427
428         if (cache != NULL)
429                 avl_get (cache, filename, (void *) &rc);
430
431         if (rc == NULL)
432         {
433                 rc = (rrd_cache_t *) malloc (sizeof (rrd_cache_t));
434                 if (rc == NULL)
435                         return (NULL);
436                 rc->values_num = 0;
437                 rc->values = NULL;
438                 rc->first_value = 0;
439                 new_rc = 1;
440         }
441
442         rc->values = (char **) realloc ((void *) rc->values,
443                         (rc->values_num + 1) * sizeof (char *));
444         if (rc->values == NULL)
445         {
446                 syslog (LOG_ERR, "rrdtool plugin: realloc failed: %s",
447                                 strerror (errno));
448                 if (cache != NULL)
449                 {
450                         void *cache_key = NULL;
451                         avl_remove (cache, filename, &cache_key, NULL);
452                         sfree (cache_key);
453                 }
454                 free (rc);
455                 return (NULL);
456         }
457
458         rc->values[rc->values_num] = strdup (value);
459         if (rc->values[rc->values_num] != NULL)
460                 rc->values_num++;
461
462         if (rc->values_num == 1)
463                 rc->first_value = time (NULL);
464
465         /* Insert if this is the first value */
466         if ((cache != NULL) && (new_rc == 1))
467         {
468                 void *cache_key = strdup (filename);
469
470                 if (cache_key == NULL)
471                 {
472                         syslog (LOG_ERR, "rrdtool plugin: strdup failed: %s",
473                                         strerror (errno));
474                         sfree (rc->values[0]);
475                         sfree (rc->values);
476                         sfree (rc);
477                         return (NULL);
478                 }
479
480                 avl_insert (cache, cache_key, rc);
481         }
482
483         DBG ("rrd_cache_insert (%s, %s) = %p", filename, value, (void *) rc);
484
485         return (rc);
486 } /* rrd_cache_t *rrd_cache_insert */
487
488 static int rrd_write_cache_entry (const char *filename, rrd_cache_t *rc)
489 {
490         char **argv;
491         int    argc;
492
493         char *fn;
494         int status;
495
496         int i;
497
498         argc = rc->values_num + 2;
499         argv = (char **) malloc ((argc + 1) * sizeof (char *));
500         if (argv == NULL)
501                 return (-1);
502
503         fn = strdup (filename);
504         if (fn == NULL)
505         {
506                 free (argv);
507                 return (-1);
508         }
509
510         argv[0] = "update";
511         argv[1] = fn;
512         memcpy (argv + 2, rc->values, rc->values_num * sizeof (char *));
513         argv[argc] = NULL;
514
515         DBG ("rrd_update (argc = %i, argv = %p)", argc, (void *) argv);
516
517         optind = 0; /* bug in librrd? */
518         rrd_clear_error ();
519         status = rrd_update (argc, argv);
520
521         free (argv);
522         free (fn);
523
524         /* Free the value list of `rc' */
525         for (i = 0; i < rc->values_num; i++)
526                 free (rc->values[i]);
527         free (rc->values);
528         rc->values = NULL;
529         rc->values_num = 0;
530
531         if (status != 0)
532         {
533                 syslog (LOG_WARNING, "rrd_update failed: %s: %s",
534                                 filename, rrd_get_error ());
535                 return (-1);
536         }
537
538         return (0);
539 } /* int rrd_write_cache_entry */
540
541 static void rrd_cache_flush (int timeout)
542 {
543         rrd_cache_t *rc;
544         time_t       now;
545
546         char **keys = NULL;
547         int    keys_num = 0;
548
549         char *key;
550         avl_iterator_t *iter;
551         int i;
552
553         if (cache == NULL)
554                 return;
555
556         DBG ("Flushing cache, timeout = %i", timeout);
557
558         now = time (NULL);
559
560         /* Build a list of entries to be flushed */
561         iter = avl_get_iterator (cache);
562         while (avl_iterator_next (iter, (void *) &key, (void *) &rc) == 0)
563         {
564                 DBG ("key = %s; age = %i;", key, now - rc->first_value);
565                 if ((now - rc->first_value) >= timeout)
566                 {
567                         keys = (char **) realloc ((void *) keys,
568                                         (keys_num + 1) * sizeof (char *));
569                         if (keys == NULL)
570                         {
571                                 DBG ("realloc failed: %s", strerror (errno));
572                                 syslog (LOG_ERR, "rrdtool plugin: "
573                                                 "realloc failed: %s",
574                                                 strerror (errno));
575                                 avl_iterator_destroy (iter);
576                                 return;
577                         }
578                         keys[keys_num] = key;
579                         keys_num++;
580                 }
581         } /* while (avl_iterator_next) */
582         avl_iterator_destroy (iter);
583         
584         for (i = 0; i < keys_num; i++)
585         {
586                 if (avl_remove (cache, keys[i], (void *) &key, (void *) &rc) != 0)
587                 {
588                         DBG ("avl_remove (%s) failed.", keys[i]);
589                         continue;
590                 }
591
592                 rrd_write_cache_entry (keys[i], rc);
593                 /* rc's value-list is free's by `rrd_write_cache_entry' */
594                 sfree (rc);
595                 sfree (key);
596                 keys[i] = NULL;
597         } /* for (i = 0..keys_num) */
598
599         free (keys);
600         DBG ("Flushed %i value(s)", keys_num);
601
602         cache_flush_last = now;
603 } /* void rrd_cache_flush */
604
605 static int rrd_write (const data_set_t *ds, const value_list_t *vl)
606 {
607         struct stat  statbuf;
608         char         filename[512];
609         char         values[512];
610         rrd_cache_t *rc;
611         time_t       now;
612
613         if (value_list_to_filename (filename, sizeof (filename), ds, vl) != 0)
614                 return (-1);
615
616         if (value_list_to_string (values, sizeof (values), ds, vl) != 0)
617                 return (-1);
618
619         if (stat (filename, &statbuf) == -1)
620         {
621                 if (errno == ENOENT)
622                 {
623                         if (rrd_create_file (filename, ds))
624                                 return (-1);
625                 }
626                 else
627                 {
628                         syslog (LOG_ERR, "stat(%s) failed: %s",
629                                         filename, strerror (errno));
630                         return (-1);
631                 }
632         }
633         else if (!S_ISREG (statbuf.st_mode))
634         {
635                 syslog (LOG_ERR, "stat(%s): Not a regular file!",
636                                 filename);
637                 return (-1);
638         }
639
640         rc = rrd_cache_insert (filename, values);
641         if (rc == NULL)
642                 return (-1);
643
644         if (cache == NULL)
645         {
646                 rrd_write_cache_entry (filename, rc);
647                 /* rc's value-list is free's by `rrd_write_cache_entry' */
648                 sfree (rc);
649                 return (0);
650         }
651
652         now = time (NULL);
653
654         DBG ("age (%s) = %i", filename, now - rc->first_value);
655
656         /* `rc' is not free'd here, because we'll likely reuse it. If not, then
657          * the next flush will remove this entry.  */
658         if ((now - rc->first_value) >= cache_timeout)
659                 rrd_write_cache_entry (filename, rc);
660
661         if ((now - cache_flush_last) >= cache_flush_timeout)
662                 rrd_cache_flush (cache_flush_timeout);
663
664         return (0);
665 } /* int rrd_write */
666
667 static int rrd_config (const char *key, const char *value)
668 {
669         if (strcasecmp ("CacheTimeout", key) == 0)
670         {
671                 int tmp = atoi (value);
672                 if (tmp < 0)
673                 {
674                         fprintf (stderr, "rrdtool: `CacheTimeout' must "
675                                         "be greater than 0.\n");
676                         return (1);
677                 }
678                 cache_timeout = tmp;
679         }
680         else if (strcasecmp ("CacheFlush", key) == 0)
681         {
682                 int tmp = atoi (value);
683                 if (tmp < 0)
684                 {
685                         fprintf (stderr, "rrdtool: `CacheFlush' must "
686                                         "be greater than 0.\n");
687                         return (1);
688                 }
689                 cache_flush_timeout = tmp;
690         }
691         else if (strcasecmp ("DataDir", key) == 0)
692         {
693                 if (datadir != NULL)
694                         free (datadir);
695                 datadir = strdup (value);
696                 if (datadir != NULL)
697                 {
698                         int len = strlen (datadir);
699                         while ((len > 0) && (datadir[len - 1] == '/'))
700                         {
701                                 len--;
702                                 datadir[len] = '\0';
703                         }
704                         if (len <= 0)
705                         {
706                                 free (datadir);
707                                 datadir = NULL;
708                         }
709                 }
710         }
711         else if (strcasecmp ("StepSize", key) == 0)
712         {
713                 int tmp = atoi (value);
714                 if (tmp <= 0)
715                 {
716                         fprintf (stderr, "rrdtool: `StepSize' must "
717                                         "be greater than 0.\n");
718                         return (1);
719                 }
720                 stepsize = tmp;
721         }
722         else if (strcasecmp ("HeartBeat", key) == 0)
723         {
724                 int tmp = atoi (value);
725                 if (tmp <= 0)
726                 {
727                         fprintf (stderr, "rrdtool: `HeartBeat' must "
728                                         "be greater than 0.\n");
729                         return (1);
730                 }
731                 heartbeat = tmp;
732         }
733         else if (strcasecmp ("RRARows", key) == 0)
734         {
735                 int tmp = atoi (value);
736                 if (tmp <= 0)
737                 {
738                         fprintf (stderr, "rrdtool: `RRARows' must "
739                                         "be greater than 0.\n");
740                         return (1);
741                 }
742                 rrarows = tmp;
743         }
744         else if (strcasecmp ("XFF", key) == 0)
745         {
746                 double tmp = atof (value);
747                 if ((tmp < 0.0) || (tmp >= 1.0))
748                 {
749                         fprintf (stderr, "rrdtool: `XFF' must "
750                                         "be in the range 0 to 1 (exclusive).");
751                         return (1);
752                 }
753                 xff = tmp;
754         }
755         else
756         {
757                 return (-1);
758         }
759         return (0);
760 } /* int rrd_config */
761
762 static int rrd_shutdown (void)
763 {
764         rrd_cache_flush (-1);
765         if (cache != NULL)
766                 avl_destroy (cache);
767         cache = NULL;
768
769         return (0);
770 } /* int rrd_shutdown */
771
772 static int rrd_init (void)
773 {
774         if (stepsize <= 0)
775                 stepsize = interval_g;
776         if (heartbeat <= 0)
777                 heartbeat = 2 * interval_g;
778
779         if (heartbeat < interval_g)
780                 syslog (LOG_WARNING, "rrdtool plugin: Your `heartbeat' is "
781                                 "smaller than your `interval'. This will "
782                                 "likely cause problems.");
783         else if (stepsize < interval_g)
784                 syslog (LOG_WARNING, "rrdtool plugin: Your `stepsize' is "
785                                 "smaller than your `interval'. This will "
786                                 "create needlessly big RRD-files.");
787
788         if (cache_timeout < 2)
789         {
790                 cache_timeout = 0;
791                 cache_flush_timeout = 0;
792         }
793         else
794         {
795                 if (cache_flush_timeout < cache_timeout)
796                         cache_flush_timeout = 10 * cache_timeout;
797
798                 cache = avl_create ((int (*) (const void *, const void *)) strcmp);
799                 cache_flush_last = time (NULL);
800                 plugin_register_shutdown ("rrdtool", rrd_shutdown);
801         }
802
803         DBG ("datadir = %s; stepsize = %i; heartbeat = %i; rrarows = %i; xff = %lf;",
804                         (datadir == NULL) ? "(null)" : datadir,
805                         stepsize, heartbeat, rrarows, xff);
806
807         return (0);
808 } /* int rrd_init */
809
810 void module_register (void)
811 {
812         plugin_register_config ("rrdtool", rrd_config,
813                         config_keys, config_keys_num);
814         plugin_register_init ("rrdtool", rrd_init);
815         plugin_register_write ("rrdtool", rrd_write);
816 }