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