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