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