]> arthur.barton.de Git - netdata.git/blob - plugins.d/charts.d.plugin
converted tabs for C, JS, BASH to 4 spaces
[netdata.git] / plugins.d / charts.d.plugin
1 #!/usr/bin/env bash
2
3 PROGRAM_FILE="$0"
4 PROGRAM_NAME="$(basename $0)"
5 PROGRAM_NAME="${PROGRAM_NAME/.plugin}"
6
7 # if you need to run parallel charts.d processes
8 # just link this files with a different name
9 # in the same directory, with a .plugin suffix
10 # netdata will start multiple of them
11 # each will have a different config file
12
13 echo >&2 "$PROGRAM_NAME: started from '$PROGRAM_FILE' with options: $*"
14
15 if [ $(( ${BASH_VERSINFO[0]} )) -lt 4 ]
16 then
17     echo >&2
18     echo >&2 "$PROGRAM_NAME: ERROR"
19     echo >&2 "BASH version 4 or later is required."
20     echo >&2 "You are running version: ${BASH_VERSION}"
21     echo >&2 "Please upgrade."
22     echo >&2
23     exit 1
24 fi
25
26 # check a few commands
27 require_cmd() {
28     which "$1" >/dev/null
29     if [ $? -ne 0 ]
30         then
31         echo >&2 "$PROGRAM_NAME: ERROR: Command '$1' is not found in the system path."
32         return 1
33     fi
34     return 0
35 }
36
37 require_cmd date || exit 1
38 require_cmd sed || exit 1
39 require_cmd basename || exit 1
40 require_cmd dirname || exit 1
41 require_cmd cat || exit 1
42 require_cmd grep || exit 1
43 require_cmd egrep || exit 1
44 require_cmd mktemp || exit 1
45 require_cmd awk || exit 1
46
47 # -----------------------------------------------------------------------------
48 # insternal defaults
49 # netdata exposes a few environment variables for us
50
51 pause_method="sleep" # use either "suspend" or "sleep"
52                      # DO NOT USE SUSPEND - LINUX WILL SUSPEND NETDATA TOO
53                      # THE WHOLE PROCESS GROUP - NOT JUST THE SHELL
54
55 pluginsd="${NETDATA_PLUGINS_DIR}"
56 [ -z "$pluginsd" ] && pluginsd="$( dirname $PROGRAM_FILE )"
57
58 confd="${NETDATA_CONFIG_DIR-/etc/netdata}"
59 chartsd="$pluginsd/../charts.d"
60
61 myconfig="$confd/$PROGRAM_NAME.conf"
62
63 minimum_update_frequency="${NETDATA_UPDATE_EVERY-1}"
64 update_every=${minimum_update_frequency}    # this will be overwritten by the command line
65
66 # work around for non BASH shells
67 charts_create="_create"
68 charts_update="_update"
69 charts_check="_check"
70 charts_undescore="_"
71
72 # when making iterations, charts.d can loop more frequently
73 # to prevent plugins missing iterations.
74 # this is a percentage relative to update_every to align its
75 # iterations.
76 # The minimum is 10%, the maximum 100%.
77 # So, if update_every is 1 second and time_divisor is 50,
78 # charts.d will iterate every 500ms.
79 # Charts will be called to collect data only if the time
80 # passed since the last time the collected data is equal or
81 # above their update_every.
82 time_divisor=50
83
84 # number of seconds to run without restart
85 # after this time, charts.d.plugin will exit
86 # netdata will restart it
87 restart_timeout=$((3600 * 4))
88
89 # check if the charts.d plugins are using global variables
90 # they should not.
91 # It does not currently support BASH v4 arrays, so it is
92 # disabled
93 dryrunner=0
94
95 # check for timeout command
96 check_for_timeout=1
97
98 # the default enable/disable value for all charts
99 enable_all_charts="yes"
100
101 # -----------------------------------------------------------------------------
102 # parse parameters
103
104 debug=0
105 check=0
106 chart_only=
107 while [ ! -z "$1" ]
108 do
109     if [ "$1" = "check" ]
110     then
111         check=1
112         shift
113         continue
114     fi
115
116     if [ "$1" = "debug" -o "$1" = "all" ]
117     then
118         debug=1
119         shift
120         continue
121     fi
122
123     if [ -f "$chartsd/$1.chart.sh" ]
124     then
125         debug=1
126         chart_only="$( echo $1.chart.sh | sed "s/\.chart\.sh$//g" )"
127         shift
128         continue
129     fi
130
131     if [ -f "$chartsd/$1" ]
132     then
133         debug=1
134         chart_only="$( echo $1 | sed "s/\.chart\.sh$//g" )"
135         shift
136         continue
137     fi
138
139     # number check
140     n="$1"
141     x=$(( n ))
142     if [ "$x" = "$n" ]
143     then
144         shift
145         update_every=$x
146         [ $update_every -lt $minimum_update_frequency ] && update_every=$minimum_update_frequency
147         continue
148     fi
149
150     echo >&2 "Cannot understand parameter $1. Aborting."
151     echo "DISABLE"
152     exit 1
153 done
154
155
156 # -----------------------------------------------------------------------------
157 # load my configuration
158
159 if [ -f "$myconfig" ]
160     then
161     . "$myconfig"
162     if [ $? -ne 0 ]
163     then
164         echo >&2 "$PROGRAM_NAME: cannot load $myconfig"
165         echo "DISABLE"
166         exit 1
167     fi
168     time_divisor=$((time_divisor))
169     [ $time_divisor -lt 10 ] && time_divisor=10
170     [ $time_divisor -gt 100 ] && time_divisor=100
171 else
172     echo >&2 "$PROGRAM_NAME: configuration file '$myconfig' not found. Using defaults."
173 fi
174
175 if [ "$pause_method" = "suspend" ]
176 then
177     # enable bash job control
178     # this is required for suspend to work
179     set -m
180 fi
181
182 # we check for the timeout command, after we load our
183 # configuration, so that the user may overwrite the
184 # timeout command we use, providing a function that
185 # can emulate the timeout command we need:
186 # > timeout SECONDS command ...
187 if [ $check_for_timeout -eq 1 ]
188     then
189     require_cmd timeout || exit 1
190 fi
191
192 # -----------------------------------------------------------------------------
193 # internal checks
194
195 # netdata passes the requested update frequency as the first argument
196 update_every=$(( update_every + 1 - 1)) # makes sure it is a number
197 test $update_every -eq 0 && update_every=1 # if it is zero, make it 1
198
199 # check the charts.d directory
200 if [ ! -d "$chartsd" ]
201     then
202     echo >&2 "$PROGRAM_NAME: cannot find charts directory '$chartsd'"
203     echo "DISABLE"
204 fi
205
206
207 # -----------------------------------------------------------------------------
208 # loop control
209
210 # default sleep function
211 LOOPSLEEPMS_HIGHRES=0
212 loopsleepms() {
213     [ "$1" = "tellwork" ] && shift
214     sleep $1
215 }
216
217 now_ms=
218 current_time_ms() {
219     now_ms="$(date +'%s')000"
220 }
221
222 # if found and included, this file overwrites loopsleepms()
223 # and current_time_ms() with a high resolution timer function
224 # for precise looping.
225 . "$pluginsd/loopsleepms.sh.inc"
226
227
228 # -----------------------------------------------------------------------------
229 # library functions
230
231 fixid() {
232     echo "$*" |\
233         tr -c "[A-Z][a-z][0-9]" "_" |\
234         sed -e "s|^_\+||g" -e "s|_\+$||g" -e "s|_\+|_|g" |\
235         tr "[A-Z]" "[a-z]"
236 }
237
238 # convert any floating point number
239 # to integer, give a multiplier
240 # the result is stored in ${FLOAT2INT_RESULT}
241 # so that no fork is necessary
242 # the multiplier must be a power of 10
243 float2int() {
244     local f m="$2" a b l v=($1)
245     f=${v[0]}
246
247     # echo >&2 "value='${1}' f='${f}', m='${m}'"
248
249     # the length of the multiplier - 1
250     l=$(( ${#m} - 1 ))
251
252     # check if the number is in scientific notation
253     if [[ ${f} =~ ^[[:space:]]*(-)?[0-9.]+(e|E)(\+|-)[0-9]+ ]]
254         then
255         # convert it to decimal
256         # unfortunately, this fork cannot be avoided
257         # if you know of a way to avoid it, please let me know
258         f=$(printf "%0.${l}f" ${f})
259     fi
260
261     # split the floating point number
262     # in integer (a) and decimal (b)
263     a=${f/.*/}
264     b=${f/*./}
265
266     # if the integer part is missing
267     # set it to zero
268     [ -z "${a}" ] && a="0"
269
270     # strip leading zeros from the integer part
271     # base 10 convertion
272     a=$((10#$a))
273
274     # check the length of the decimal part
275     # against the length of the multiplier
276     if [ ${#b} -gt ${l} ]
277         then
278         # too many digits - take the most significant
279         b=${b:0:${l}}
280
281     elif [ ${#b} -lt ${l} ]
282         then
283         # too few digits - pad with zero on the right
284         local z="00000000000000000000000" r=$((l - ${#b}))
285         b="${b}${z:0:${r}}"
286     fi
287
288     # strip leading zeros from the decimal part
289     # base 10 convertion
290     b=$((10#$b))
291
292     # store the result
293     FLOAT2INT_RESULT=$(( (a * m) + b ))
294     #echo >&2 "FLOAT2INT_RESULT='${FLOAT2INT_RESULT}'"
295 }
296
297
298 # -----------------------------------------------------------------------------
299 # charts check functions
300
301 all_charts() {
302     cd "$chartsd"
303     [ $? -ne 0 ] && echo >&2 "$PROGRAM_NAME: Cannot cd to $chartsd" && return 1
304
305     ls *.chart.sh | sed "s/\.chart\.sh$//g"
306 }
307
308 declare -A charts_enable_keyword=(
309           ['apache']="force"
310         ['cpu_apps']="force"
311          ['cpufreq']="force"
312          ['example']="force"
313             ['exim']="force"
314          ['hddtemp']="force"
315     ['load_average']="force"
316         ['mem_apps']="force"
317            ['mysql']="force"
318            ['nginx']="force"
319           ['phpfpm']="force"
320          ['postfix']="force"
321          ['sensors']="force"
322            ['squid']="force"
323           ['tomcat']="force"
324     )
325
326 all_enabled_charts() {
327     local charts= enabled= required=
328
329     # find all enabled charts
330
331     for chart in $( all_charts )
332     do
333         eval "enabled=\$$chart"
334         if [ -z "${enabled}" ]
335             then
336             enabled="${enable_all_charts}"
337         fi
338
339         required="${charts_enable_keyword[${chart}]}"
340         [ -z "${required}" ] && required="yes"
341
342         if [ ! "${enabled}" = "${required}" ]
343         then
344             echo >&2 "$PROGRAM_NAME: '$chart' is NOT enabled. Add a line with $chart=$required in $myconfig to enable it (or remove the line that disables it)."
345         else
346             [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: '$chart' is enabled."
347             local charts="$charts $chart"
348         fi
349     done
350
351     local charts2=
352     for chart in $charts
353     do
354         # check the enabled charts
355         local check="$( cat "$chartsd/$chart.chart.sh" | sed "s/^ \+//g" | grep "^$chart$charts_check()" )"
356         if [ -z "$check" ]
357         then
358             echo >&2 "$PROGRAM_NAME: chart '$chart' does not seem to have a $chart$charts_check() function. Disabling it."
359             continue
360         fi
361
362         local create="$( cat "$chartsd/$chart.chart.sh" | sed "s/^ \+//g" | grep "^$chart$charts_create()" )"
363         if [ -z "$create" ]
364         then
365             echo >&2 "$PROGRAM_NAME: chart '$chart' does not seem to have a $chart$charts_create() function. Disabling it."
366             continue
367         fi
368
369         local update="$( cat "$chartsd/$chart.chart.sh" | sed "s/^ \+//g" | grep "^$chart$charts_update()" )"
370         if [ -z "$update" ]
371         then
372             echo >&2 "$PROGRAM_NAME: chart '$chart' does not seem to have a $chart$charts_update() function. Disabling it."
373             continue
374         fi
375
376         # check its config
377         #if [ -f "$confd/$chart.conf" ]
378         #then
379         #   if [ ! -z "$( cat "$confd/$chart.conf" | sed "s/^ \+//g" | grep -v "^$" | grep -v "^#" | grep -v "^$chart$charts_undescore" )" ]
380         #   then
381         #       echo >&2 "$PROGRAM_NAME: chart's $chart config $confd/$chart.conf should only have lines starting with $chart$charts_undescore . Disabling it."
382         #       continue
383         #   fi
384         #fi
385
386         #if [ $dryrunner -eq 1 ]
387         #   then
388         #   "$pluginsd/charts.d.dryrun-helper.sh" "$chart" "$chartsd/$chart.chart.sh" "$confd/$chart.conf" >/dev/null
389         #   if [ $? -ne 0 ]
390         #   then
391         #       echo >&2 "$PROGRAM_NAME: chart's $chart did not pass the dry run check. This means it uses global variables not starting with $chart. Disabling it."
392         #       continue
393         #   fi
394         #fi
395
396         local charts2="$charts2 $chart"
397     done
398
399     echo $charts2
400     [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: enabled charts: $charts2"
401 }
402
403
404 # -----------------------------------------------------------------------------
405 # load the charts
406
407 suffix_update_every="_update_every"
408 active_charts=
409 for chart in $( all_enabled_charts )
410 do
411     [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: loading chart: '$chartsd/$chart.chart.sh'"
412     . "$chartsd/$chart.chart.sh"
413
414     if [ -f "$confd/charts.d/$chart.conf" ]
415     then
416         [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: loading chart options: '$confd/charts.d/$chart.conf'"
417         . "$confd/charts.d/$chart.conf"
418     elif [ -f "$confd/$chart.conf" ]
419     then
420         [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: loading chart options: '$confd/$chart.conf'"
421         . "$confd/$chart.conf"
422     else
423         echo >&2 "$PROGRAM_NAME: $chart: configuration file '$confd/charts.d/$chart.conf' not found. Using defaults."
424     fi
425
426     eval "dt=\$$chart$suffix_update_every"
427     dt=$(( dt + 1 - 1 )) # make sure it is a number
428     if [ $dt -lt $update_every ]
429     then
430         eval "$chart$suffix_update_every=$update_every"
431     fi
432
433     $chart$charts_check
434     if [ $? -eq 0 ]
435     then
436         [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: '$chart' activated"
437         active_charts="$active_charts $chart"
438     else
439         echo >&2 "$PROGRAM_NAME: chart '$chart' check() function reports failure."
440     fi
441 done
442 [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: activated charts: $active_charts"
443
444
445 # -----------------------------------------------------------------------------
446 # check overwrites
447
448 # enable work time reporting
449 debug_time=
450 test $debug -eq 1 && debug_time=tellwork
451
452 # if we only need a specific chart, remove all the others
453 if [ ! -z "${chart_only}" ]
454 then
455     [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: requested to run only for: '${chart_only}'"
456     check_charts=
457     for chart in $active_charts
458     do
459         if [ "$chart" = "$chart_only" ]
460         then
461             check_charts="$chart"
462             break
463         fi
464     done
465     active_charts="$check_charts"
466 fi
467 [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: activated charts: $active_charts"
468
469 # stop if we just need a pre-check
470 if [ $check -eq 1 ]
471 then
472     echo >&2 "CHECK RESULT"
473     echo >&2 "Will run the charts: $active_charts"
474     exit 0
475 fi
476
477 # -----------------------------------------------------------------------------
478 # create temp dir
479
480 TMP_DIR=
481 chartsd_cleanup() {
482     cd /tmp
483     if [ ! -z "$TMP_DIR" -a -d "$TMP_DIR" ]
484     then
485         [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: cleaning up temporary directory $TMP_DIR ..."
486         rm -rf "$TMP_DIR"
487     fi
488     exit 0
489 }
490 trap chartsd_cleanup EXIT
491 trap chartsd_cleanup SIGHUP
492 trap chartsd_cleanup INT
493
494 if [ $UID = "0" ]
495 then
496     TMP_DIR="$( mktemp -d /var/run/netdata-${PROGRAM_NAME}-XXXXXXXXXX )"
497 else
498     TMP_DIR="$( mktemp -d /tmp/.netdata-${PROGRAM_NAME}-XXXXXXXXXX )"
499 fi
500
501 cd "$TMP_DIR" || exit 1
502
503 # -----------------------------------------------------------------------------
504 # create charts
505
506 run_charts=
507 for chart in $active_charts
508 do
509     [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: Calling '$chart$charts_create()'..."
510     $chart$charts_create
511     if [ $? -eq 0 ]
512     then
513         run_charts="$run_charts $chart"
514         [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: '$chart' has initialized."
515     else
516         echo >&2 "$PROGRAM_NAME: chart '$chart' function '$chart$charts_create()' reports failure."
517     fi
518 done
519 [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: run_charts='$run_charts'"
520
521
522 # -----------------------------------------------------------------------------
523 # update dimensions
524
525 if [ -z "$run_charts" ]
526     then
527     echo >&2 "$PROGRAM_NAME: No charts to collect data from."
528     echo "DISABLE"
529     exit 1
530 fi
531
532 declare -A charts_last_update=() charts_update_every=() charts_next_update=() charts_run_counter=() charts_serial_failures=()
533 global_update() {
534     local exit_at \
535         c=0 dt ret last_ms exec_start_ms exec_end_ms \
536         chart now_charts=() next_charts=($run_charts) \
537         next_ms x seconds millis
538
539     # return the current time in ms in $now_ms
540     current_time_ms
541
542     exit_at=$(( now_ms + (restart_timeout * 1000) ))
543
544     for chart in $run_charts
545     do
546         eval "charts_update_every[$chart]=\$$chart$suffix_update_every"
547         test -z "${charts_update_every[$chart]}" && charts_update_every[$charts]=$update_every
548         charts_last_update[$chart]=$((now_ms - (now_ms % (charts_update_every[$chart] * 1000) ) ))
549         charts_next_update[$chart]=$(( charts_last_update[$chart] + (charts_update_every[$chart] * 1000) ))
550         charts_run_counter[$chart]=0
551         charts_serial_failures[$chart]=0
552
553         echo "CHART netdata.plugin_chartsd_$chart '' 'Execution time for $chart plugin' 'milliseconds / run' charts.d netdata.plugin_charts area 145000 ${charts_update_every[$chart]}"
554         echo "DIMENSION run_time 'run time' absolute 1 1"
555     done
556
557     # the main loop
558     while [ "${#next_charts[@]}" -gt 0 ]
559     do
560         c=$((c + 1))
561         now_charts=("${next_charts[@]}")
562         next_charts=()
563
564         # return the current time in ms in $now_ms
565         current_time_ms
566
567         for chart in "${now_charts[@]}"
568         do
569             # echo >&2 "DEBUG: chart: $chart last: ${charts_last_update[$chart]}, next: ${charts_next_update[$chart]}, now: ${now_ms}"
570             if [ ${now_ms} -ge ${charts_next_update[$chart]} ]
571             then
572                 last_ms=${charts_last_update[$chart]}
573                 dt=$(( (now_ms - last_ms) ))
574                 # echo >&2 "DEBUG: chart: $chart last: ${charts_last_update[$chart]}, next: ${charts_next_update[$chart]}, now: ${now_ms}, dt: ${dt}"
575
576                 charts_last_update[$chart]=${now_ms}
577
578                 while [ ${charts_next_update[$chart]} -lt ${now_ms} ]
579                 do
580                     charts_next_update[$chart]=$(( charts_next_update[$chart] + (charts_update_every[$chart] * 1000) ))
581                 done
582
583                 # the first call should not give a duration
584                 # so that netdata calibrates to current time
585                 dt=$(( dt * 1000 ))
586                 charts_run_counter[$chart]=$(( charts_run_counter[$chart] + 1 ))
587                 if [ ${charts_run_counter[$chart]} -eq 1 ]
588                     then
589                     dt=
590                 fi
591
592                 exec_start_ms=$now_ms
593                 $chart$charts_update $dt
594                 ret=$?
595
596                 # return the current time in ms in $now_ms
597                 current_time_ms; exec_end_ms=$now_ms
598
599                 echo "BEGIN netdata.plugin_chartsd_$chart $dt"
600                 echo "SET run_time = $(( exec_end_ms - exec_start_ms ))"
601                 echo "END"
602
603                 if [ $ret -eq 0 ]
604                 then
605                     charts_serial_failures[$chart]=0
606                     next_charts+=($chart)
607                 else
608                     charts_serial_failures[$chart]=$(( charts_serial_failures[$chart] + 1 ))
609
610                     if [ ${charts_serial_failures[$chart]} -gt 10 ]
611                         then
612                         echo >&2 "$PROGRAM_NAME: chart '$chart' update() function reported failure ${charts_serial_failures[$chart]} times. Disabling it."
613                     else
614                         echo >&2 "$PROGRAM_NAME: chart '$chart' update() function reports failure. Will keep trying for a while."
615                         next_charts+=($chart)
616                     fi
617                 fi
618             else
619                 next_charts+=($chart)
620             fi
621         done
622
623         if [ "$pause_method" = "suspend" ]
624         then
625             echo "STOPPING_WAKE_ME_UP_PLEASE"
626             suspend || ( echo >&2 "$PROGRAM_NAME: suspend returned error $?, falling back to sleep."; loopsleepms $debug_time $update_every $time_divisor)
627         else
628             # wait the time you are required to
629             next_ms=$((now_ms + (update_every * 1000 * 100) ))
630             for x in "${charts_next_update[@]}"; do [ ${x} -lt ${next_ms} ] && next_ms=${x}; done
631             next_ms=$((next_ms - now_ms))
632
633             if [ ${LOOPSLEEPMS_HIGHRES} -eq 1 -a ${next_ms} -gt 0 ]
634                 then
635                 seconds=$(( next_ms / 1000 ))
636                 millis=$(( next_ms % 1000 ))
637                 [ ${millis} -lt 10  ] && millis="0${millis}"
638                 [ ${millis} -lt 100 ] && millis="0${millis}"
639                 [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: sleeping for ${seconds}.${millis} seconds."
640                 sleep ${seconds}.${millis}
641             else
642                 sleep $update_every
643             fi
644         fi
645
646         test ${now_ms} -ge ${exit_at} && exit 0
647     done
648
649     echo >&2 "$PROGRAM_NAME: Nothing left to do. Disabling charts.d.plugin."
650     echo "DISABLE"
651 }
652
653 global_update