diff options
Diffstat (limited to 'plugins.d')
-rw-r--r-- | plugins.d/Makefile.am | 2 | ||||
-rw-r--r-- | plugins.d/Makefile.in | 4 | ||||
-rwxr-xr-x | plugins.d/alarm-email.sh | 264 | ||||
-rwxr-xr-x | plugins.d/cgroup-name.sh | 95 | ||||
-rwxr-xr-x | plugins.d/charts.d.dryrun-helper.sh | 48 | ||||
-rwxr-xr-x | plugins.d/charts.d.plugin | 789 | ||||
-rw-r--r-- | plugins.d/loopsleepms.sh.inc | 108 | ||||
-rwxr-xr-x | plugins.d/node.d.plugin | 361 | ||||
-rwxr-xr-x | plugins.d/python.d.plugin | 533 | ||||
-rwxr-xr-x | plugins.d/tc-qos-helper.sh | 141 |
10 files changed, 1611 insertions, 734 deletions
diff --git a/plugins.d/Makefile.am b/plugins.d/Makefile.am index a717cbed1..b8a28610a 100644 --- a/plugins.d/Makefile.am +++ b/plugins.d/Makefile.am @@ -8,10 +8,12 @@ dist_plugins_DATA = \ $(NULL) dist_plugins_SCRIPTS = \ + alarm-email.sh \ cgroup-name.sh \ charts.d.dryrun-helper.sh \ charts.d.plugin \ node.d.plugin \ + python.d.plugin \ tc-qos-helper.sh \ loopsleepms.sh.inc \ $(NULL) diff --git a/plugins.d/Makefile.in b/plugins.d/Makefile.in index c74997688..06211d51c 100644 --- a/plugins.d/Makefile.in +++ b/plugins.d/Makefile.in @@ -263,6 +263,8 @@ pluginsdir = @pluginsdir@ prefix = @prefix@ program_transform_name = @program_transform_name@ psdir = @psdir@ +pythondir = @pythondir@ +registrydir = @registrydir@ sbindir = @sbindir@ sharedstatedir = @sharedstatedir@ srcdir = @srcdir@ @@ -283,10 +285,12 @@ dist_plugins_DATA = \ $(NULL) dist_plugins_SCRIPTS = \ + alarm-email.sh \ cgroup-name.sh \ charts.d.dryrun-helper.sh \ charts.d.plugin \ node.d.plugin \ + python.d.plugin \ tc-qos-helper.sh \ loopsleepms.sh.inc \ $(NULL) diff --git a/plugins.d/alarm-email.sh b/plugins.d/alarm-email.sh new file mode 100755 index 000000000..78c79ccdb --- /dev/null +++ b/plugins.d/alarm-email.sh @@ -0,0 +1,264 @@ +#!/usr/bin/env bash + +me="${0}" + +sendmail="$(which sendmail 2>/dev/null || command -v sendmail 2>/dev/null)" +if [ -z "${sendmail}" ] +then + echo >&2 "I cannot send emails - there is no sendmail command available." +fi + +sendmail_from_pipe() { + "${sendmail}" -t + + if [ $? -eq 0 ] + then + echo >&2 "${me}: Sent notification email for ${status} on '${chart}.${name}'" + return 0 + else + echo >&2 "${me}: FAILED to send notification email for ${status} on '${chart}.${name}'" + return 1 + fi +} + +name="${1}" # the name of the alarm, as given in netdata health.d entries +chart="${2}" # the name of the chart (type.id) +family="${3}" # the family of the chart +status="${4}" # the current status : UNITIALIZED, UNDEFINED, CLEAR, WARNING, CRITICAL +old_status="${5}" # the previous status: UNITIALIZED, UNDEFINED, CLEAR, WARNING, CRITICAL +value="${6}" # the current value +old_value="${7}" # the previous value +src="${8}" # the line number and file the alarm has been configured +duration="${9}" # the duration in seconds the previous state took +non_clear_duration="${10}" # the total duration in seconds this is non-clear +units="${11}" # the units of the value +info="${12}" # a short description of the alarm + +[ ! -z "${info}" ] && info=" <small><br/>${info}</small>" + +# get the system hostname +hostname="${NETDATA_HOSTNAME}" +[ -z "${hostname}" ] && hostname="${NETDATA_REGISTRY_HOSTNAME}" +[ -z "${hostname}" ] && hostname="$(hostname)" + +goto_url="${NETDATA_REGISTRY_URL}/goto-host-from-alarm.html?machine_guid=${NETDATA_REGISTRY_UNIQUE_ID}&chart=${chart}&family=${family}" + +# get the current date +date="$(date)" + +duration4human() { + local s="${1}" d=0 h=0 m=0 ds="day" hs="hour" ms="minute" ss="second" + d=$(( s / 86400 )) + s=$(( s - (d * 86400) )) + h=$(( s / 3600 )) + s=$(( s - (h * 3600) )) + m=$(( s / 60 )) + s=$(( s - (m * 60) )) + + if [ ${d} -gt 0 ] + then + [ ${m} -ge 30 ] && h=$(( h + 1 )) + [ ${d} -gt 1 ] && ds="days" + [ ${h} -gt 1 ] && hs="hours" + if [ ${h} -gt 0 ] + then + echo "${d} ${ds} and ${h} ${hs}" + else + echo "${d} ${ds}" + fi + elif [ ${h} -gt 0 ] + then + [ ${s} -ge 30 ] && m=$(( m + 1 )) + [ ${h} -gt 1 ] && hs="hours" + [ ${m} -gt 1 ] && ms="minutes" + if [ ${m} -gt 0 ] + then + echo "${h} ${hs} and ${m} ${ms}" + else + echo "${h} ${hs}" + fi + elif [ ${m} -gt 0 ] + then + [ ${m} -gt 1 ] && ms="minutes" + [ ${s} -gt 1 ] && ss="seconds" + if [ ${s} -gt 0 ] + then + echo "${m} ${ms} and ${s} ${ss}" + else + echo "${m} ${ms}" + fi + else + [ ${s} -gt 1 ] && ss="seconds" + echo "${s} ${ss}" + fi +} + +severity="${status}" +raised_for="<br/>(was ${old_status,,} for $(duration4human ${duration}))" +status_message="status unknown" +color="grey" +alarm="${name} = ${value} ${units}" + +# prepare the title based on status +case "${status}" in + CRITICAL) + status_message="is critical" + color="#ca414b" + ;; + + WARNING) + status_message="needs attention" + color="#caca4b" + ;; + + CLEAR) + status_message="recovered" + color="#77ca6d" + + # don't show the value when the status is CLEAR + # for certain alarms, this value might not have any meaning + alarm="${name}" + ;; +esac + +if [ "${status}" != "WARNING" -a "${status}" != "CRITICAL" -a "${status}" != "CLEAR" ] +then + # don't do anything if this is not WARNING, CRITICAL or CLEAR + echo >&2 "${me}: not sending notification email for ${status} on '${chart}.${name}'" + exit 0 +elif [ "${old_status}" != "WARNING" -a "${old_status}" != "CRITICAL" -a "${status}" = "CLEAR" ] +then + # don't do anything if this is CLEAR, but it was not WARNING or CRITICAL + echo >&2 "${me}: not sending notification email for ${status} on '${chart}.${name}' (last status was ${old_status})" + exit 0 +elif [ "${status}" = "CLEAR" ] +then + severity="Recovered from ${old_status}" + if [ $non_clear_duration -gt $duration ] + then + raised_for="<br/>(had issues for $(duration4human ${non_clear_duration}))" + fi + +elif [ "${old_status}" = "WARNING" -a "${status}" = "CRITICAL" ] +then + severity="Escalated to ${status}" + if [ $non_clear_duration -gt $duration ] + then + raised_for="<br/>(has issues for $(duration4human ${non_clear_duration}))" + fi + +elif [ "${old_status}" = "CRITICAL" -a "${status}" = "WARNING" ] +then + severity="Demoted to ${status}" + if [ $non_clear_duration -gt $duration ] + then + raised_for="<br/>(has issues for $(duration4human ${non_clear_duration}))" + fi + +else + raised_for= +fi + +# send the email +cat <<EOF | sendmail_from_pipe +To: root +Subject: ${hostname} ${status_message} - ${chart}.${name} +Content-Type: text/html + +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;"> +<body style="font-family:'Helvetica Neue','Helvetica',Helvetica,Arial,sans-serif;font-size:14px;width:100%!important;min-height:100%;line-height:1.6;background:#f6f6f6;margin:0;padding:0"> +<table> + <tbody> + <tr> + <td style="vertical-align:top;" valign="top"></td> + <td width="700" style="vertical-align:top;display:block!important;max-width:700px!important;clear:both!important;margin:0 auto;padding:0" valign="top"> + <div style="max-width:700px;display:block;margin:0 auto;padding:20px"> + <table width="100%" cellpadding="0" cellspacing="0" + style="background:#fff;border:1px solid #e9e9e9"> + <tbody> + <tr> + <td bgcolor="#eee" + style="padding: 5px 20px 5px 20px;background-color:#eee;"> + <div style="font-size:20px;color:#777;font-weight: bold;">netdata notification</div> + </td> + </tr> + <tr> + <td bgcolor="${color}" + style="font-size:16px;vertical-align:top;font-weight:400;text-align:center;margin:0;padding:10px;color:#ffffff;background:${color}!important;border:1px solid ${color};border-top-color:${color}" align="center" valign="top"> + <h1 style="font-weight:400;margin:0">${hostname} ${status_message}</h1> + </td> + </tr> + <tr> + <td style="vertical-align:top" valign="top"> + <div style="margin:0;padding:20px;max-width:700px"> + <table width="100%" cellpadding="0" cellspacing="0" style="max-width:700px"> + <tbody> + <tr> + <td style="font-size:18px;vertical-align:top;margin:0;padding:0 0 20px" + align="left" valign="top"> + <span>${chart}</span> + <span style="display:block;color:#666666;font-size:12px;font-weight:300;line-height:1;text-transform:uppercase">Chart</span> + </td> + </tr> + <tr style="margin:0;padding:0"> + <td style="font-size:18px;vertical-align:top;margin:0;padding:0 0 20px" + align="left" valign="top"> + <span><b>${alarm}</b>${info}</span> + <span style="display:block;color:#666666;font-size:12px;font-weight:300;line-height:1;text-transform:uppercase">Alarm</span> + </td> + </tr> + <tr> + <td style="font-size:18px;vertical-align:top;margin:0;padding:0 0 20px" + align="left" valign="top"> + <span>${family}</span> + <span style="display:block;color:#666666;font-size:12px;font-weight:300;line-height:1;text-transform:uppercase">Family</span> + </td> + </tr> + <tr style="margin:0;padding:0"> + <td style="font-size:18px;vertical-align:top;margin:0;padding:0 0 20px" + align="left" valign="top"> + <span>${severity}</span> + <span style="display:block;color:#666666;font-size:12px;font-weight:300;line-height:1;text-transform:uppercase">Severity</span> + </td> + </tr> + <tr style="margin:0;padding:0"> + <td style="font-size:18px;vertical-align:top;margin:0;padding:0 0 20px" + align="left" valign="top"><span>${date}</span> + <span>${raised_for}</span> <span + style="display:block;color:#666666;font-size:12px;font-weight:300;line-height:1;text-transform:uppercase">Time</span> + </td> + </tr> + <!-- + <tr style="margin:0;padding:0"> + <td style="font-size:18px;vertical-align:top;margin:0;padding:0 0 20px"> + <a href="${goto_url}" style="font-size:14px;color:#ffffff;text-decoration:none;line-height:1.5;font-weight:bold;text-align:center;display:inline-block;text-transform:capitalize;background:#35568d;border-width:1px;border-style:solid;border-color:#2b4c86;margin:0;padding:10px 15px" target="_blank">View Netdata</a> + </td> + </tr> + --> + <tr style="text-align:center;margin:0;padding:0"> + <td style="font-size:11px;vertical-align:top;margin:0;padding:10px 0 0 0;color:#666666" + align="center" valign="bottom">The source of this alarm is line <code>${src}</code> + </td> + </tr> + <tr style="text-align:center;margin:0;padding:0"> + <td style="font-size:12px;vertical-align:top;margin:0;padding:20px 0 0 0;color:#666666;border-top:1px solid #f0f0f0" + align="center" valign="bottom">Sent by + <a href="https://mynetdata.io/" target="_blank">netdata</a>, the real-time performance monitoring. + </td> + </tr> + </tbody> + </table> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </td> + </tr> + </tbody> +</table> +</body> +</html> +EOF diff --git a/plugins.d/cgroup-name.sh b/plugins.d/cgroup-name.sh index 8ce64b3d7..8bfc984c2 100755 --- a/plugins.d/cgroup-name.sh +++ b/plugins.d/cgroup-name.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash export PATH="${PATH}:/sbin:/usr/sbin:/usr/local/sbin" export LC_ALL=C @@ -9,66 +9,69 @@ CGROUP="${1}" NAME= if [ -z "${CGROUP}" ] - then - echo >&2 "${0}: called without a cgroup name. Nothing to do." - exit 1 + then + echo >&2 "${0}: called without a cgroup name. Nothing to do." + exit 1 fi if [ -f "${CONFIG}" ] - then - NAME="$(cat "${CONFIG}" | grep "^${CGROUP} " | sed "s/[[:space:]]\+/ /g" | cut -d ' ' -f 2)" - if [ -z "${NAME}" ] - then - echo >&2 "${0}: cannot find cgroup '${CGROUP}' in '${CONFIG}'." - fi + then + NAME="$(grep "^${CGROUP} " "${CONFIG}" | sed "s/[[:space:]]\+/ /g" | cut -d ' ' -f 2)" + if [ -z "${NAME}" ] + then + echo >&2 "${0}: cannot find cgroup '${CGROUP}' in '${CONFIG}'." + fi #else -# echo >&2 "${0}: configuration file '${CONFIG}' is not available." +# echo >&2 "${0}: configuration file '${CONFIG}' is not available." fi function get_name_classic { - DOCKERID=$1 - echo >&2 "Running command: docker ps --filter=id=\"${DOCKERID}\" --format=\"{{.Names}}\"" - NAME="$( docker ps --filter=id="${DOCKERID}" --format="{{.Names}}" )" + local DOCKERID="$1" + echo >&2 "Running command: docker ps --filter=id=\"${DOCKERID}\" --format=\"{{.Names}}\"" + NAME="$( docker ps --filter=id="${DOCKERID}" --format="{{.Names}}" )" + return 0 } function get_name_api { - DOCKERID=$1 - if [ ! -S "/var/run/docker.sock" ] - then - echo >&2 "Can't find /var/run/docker.sock" - return - fi - echo >&2 "Running API command: /containers/${DOCKERID}/json" - JSON=$(echo -e "GET /containers/${DOCKERID}/json HTTP/1.0\r\n" | nc -U /var/run/docker.sock | egrep '^{.*') - NAME=$(echo $JSON | jq -r .Name,.Config.Hostname | grep -v null | head -n1 | sed 's|^/||') + local DOCKERID="$1" + if [ ! -S "/var/run/docker.sock" ] + then + echo >&2 "Can't find /var/run/docker.sock" + return 1 + fi + echo >&2 "Running API command: /containers/${DOCKERID}/json" + JSON=$(echo -e "GET /containers/${DOCKERID}/json HTTP/1.0\r\n" | nc -U /var/run/docker.sock | egrep '^{.*') + NAME=$(echo $JSON | jq -r .Name,.Config.Hostname | grep -v null | head -n1 | sed 's|^/||') + return 0 } if [ -z "${NAME}" ] - then - if [[ "${CGROUP}" =~ ^.*docker[-/\.][a-fA-F0-9]+[-\.]?.*$ ]] - then - DOCKERID="$( echo "${CGROUP}" | sed "s|^.*docker[-/]\([a-fA-F0-9]\+\)[-\.]\?.*$|\1|" )" + then + if [[ "${CGROUP}" =~ ^.*docker[-_/\.][a-fA-F0-9]+[-_\.]?.*$ ]] + then + DOCKERID="$( echo "${CGROUP}" | sed "s|^.*docker[-_/]\([a-fA-F0-9]\+\)[-_\.]\?.*$|\1|" )" + # echo "DOCKERID=${DOCKERID}" - if [ ! -z "${DOCKERID}" -a \( ${#DOCKERID} -eq 64 -o ${#DOCKERID} -eq 12 \) ] - then - if hash docker 2>/dev/null - then - get_name_classic $DOCKERID - else - get_name_api $DOCKERID - fi - if [ -z "${NAME}" ] - then - echo >&2 "Cannot find the name of docker container '${DOCKERID}'" - NAME="${DOCKERID:0:12}" - else - echo >&2 "Docker container '${DOCKERID}' is named '${NAME}'" - fi - fi - fi + if [ ! -z "${DOCKERID}" -a \( ${#DOCKERID} -eq 64 -o ${#DOCKERID} -eq 12 \) ] + then + if hash docker 2>/dev/null + then + get_name_classic $DOCKERID + else + get_name_api $DOCKERID || get_name_classic $DOCKERID + fi + if [ -z "${NAME}" ] + then + echo >&2 "Cannot find the name of docker container '${DOCKERID}'" + NAME="${DOCKERID:0:12}" + else + echo >&2 "Docker container '${DOCKERID}' is named '${NAME}'" + fi + fi + fi - [ -z "${NAME}" ] && NAME="${CGROUP}" - [ ${#NAME} -gt 50 ] && NAME="${NAME:0:50}" + [ -z "${NAME}" ] && NAME="${CGROUP}" + [ ${#NAME} -gt 50 ] && NAME="${NAME:0:50}" fi echo >&2 "${0}: cgroup '${CGROUP}' is called '${NAME}'" diff --git a/plugins.d/charts.d.dryrun-helper.sh b/plugins.d/charts.d.dryrun-helper.sh index 646452892..8142f9882 100755 --- a/plugins.d/charts.d.dryrun-helper.sh +++ b/plugins.d/charts.d.dryrun-helper.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # will stop the script for any error set -e @@ -14,7 +14,7 @@ tmp1="`mktemp`" tmp2="`mktemp`" myset() { - set | grep -v "^_=" | grep -v "^PIPESTATUS=" | grep -v "^BASH_LINENO=" + set | grep -v "^_=" | grep -v "^PIPESTATUS=" | grep -v "^BASH_LINENO=" } # save 2 'set' @@ -25,9 +25,9 @@ myset >"$tmp2" diff "$tmp1" "$tmp2" >/dev/null 2>&1 if [ $? -ne 0 ] then - # they differ, we cannot do the check - echo >&2 "$me: cannot check with diff." - can_diff=0 + # they differ, we cannot do the check + echo >&2 "$me: cannot check with diff." + can_diff=0 fi # do it again, now including the script @@ -36,21 +36,21 @@ myset >"$tmp1" # include the plugin and its config if [ -f "$conf" ] then - . "$conf" - if [ $? -ne 0 ] - then - echo >&2 "$me: cannot load config file $conf" - rm "$tmp1" "$tmp2" - exit 1 - fi + . "$conf" + if [ $? -ne 0 ] + then + echo >&2 "$me: cannot load config file $conf" + rm "$tmp1" "$tmp2" + exit 1 + fi fi . "$chart" if [ $? -ne 0 ] then - echo >&2 "$me: cannot load chart file $chart" - rm "$tmp1" "$tmp2" - exit 1 + echo >&2 "$me: cannot load chart file $chart" + rm "$tmp1" "$tmp2" + exit 1 fi # remove all variables starting with the plugin name @@ -58,15 +58,15 @@ myset | grep -v "^$name" >"$tmp2" if [ $can_diff -eq 1 ] then - # check if they are different - # make sure they don't differ - diff "$tmp1" "$tmp2" >&2 - if [ $? -ne 0 ] - then - # they differ - rm "$tmp1" "$tmp2" - exit 1 - fi + # check if they are different + # make sure they don't differ + diff "$tmp1" "$tmp2" >&2 + if [ $? -ne 0 ] + then + # they differ + rm "$tmp1" "$tmp2" + exit 1 + fi fi rm "$tmp1" "$tmp2" diff --git a/plugins.d/charts.d.plugin b/plugins.d/charts.d.plugin index 2824fa3c6..9aaadc168 100755 --- a/plugins.d/charts.d.plugin +++ b/plugins.d/charts.d.plugin @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PROGRAM_FILE="$0" PROGRAM_NAME="$(basename $0)" @@ -14,24 +14,24 @@ echo >&2 "$PROGRAM_NAME: started from '$PROGRAM_FILE' with options: $*" if [ $(( ${BASH_VERSINFO[0]} )) -lt 4 ] then - echo >&2 - echo >&2 "$PROGRAM_NAME: ERROR" - echo >&2 "BASH version 4 or later is required." - echo >&2 "You are running version: ${BASH_VERSION}" - echo >&2 "Please upgrade." - echo >&2 - exit 1 + echo >&2 + echo >&2 "$PROGRAM_NAME: ERROR" + echo >&2 "BASH version 4 or later is required." + echo >&2 "You are running version: ${BASH_VERSION}" + echo >&2 "Please upgrade." + echo >&2 + exit 1 fi # check a few commands require_cmd() { - which "$1" >/dev/null - if [ $? -ne 0 ] - then - echo >&2 "$PROGRAM_NAME: ERROR: Command '$1' is not found in the system path." - return 1 - fi - return 0 + which "$1" >/dev/null + if [ $? -ne 0 ] + then + echo >&2 "$PROGRAM_NAME: ERROR: Command '$1' is not found in the system path." + return 1 + fi + return 0 } require_cmd date || exit 1 @@ -61,7 +61,7 @@ chartsd="$pluginsd/../charts.d" myconfig="$confd/$PROGRAM_NAME.conf" minimum_update_frequency="${NETDATA_UPDATE_EVERY-1}" -update_every=${minimum_update_frequency} # this will be overwritten by the command line +update_every=${minimum_update_frequency} # this will be overwritten by the command line # work around for non BASH shells charts_create="_create" @@ -106,50 +106,50 @@ check=0 chart_only= while [ ! -z "$1" ] do - if [ "$1" = "check" ] - then - check=1 - shift - continue - fi - - if [ "$1" = "debug" -o "$1" = "all" ] - then - debug=1 - shift - continue - fi - - if [ -f "$chartsd/$1.chart.sh" ] - then - debug=1 - chart_only="$( echo $1.chart.sh | sed "s/\.chart\.sh$//g" )" - shift - continue - fi - - if [ -f "$chartsd/$1" ] - then - debug=1 - chart_only="$( echo $1 | sed "s/\.chart\.sh$//g" )" - shift - continue - fi - - # number check - n="$1" - x=$(( n )) - if [ "$x" = "$n" ] - then - shift - update_every=$x - [ $update_every -lt $minimum_update_frequency ] && update_every=$minimum_update_frequency - continue - fi - - echo >&2 "Cannot understand parameter $1. Aborting." - echo "DISABLE" - exit 1 + if [ "$1" = "check" ] + then + check=1 + shift + continue + fi + + if [ "$1" = "debug" -o "$1" = "all" ] + then + debug=1 + shift + continue + fi + + if [ -f "$chartsd/$1.chart.sh" ] + then + debug=1 + chart_only="$( echo $1.chart.sh | sed "s/\.chart\.sh$//g" )" + shift + continue + fi + + if [ -f "$chartsd/$1" ] + then + debug=1 + chart_only="$( echo $1 | sed "s/\.chart\.sh$//g" )" + shift + continue + fi + + # number check + n="$1" + x=$(( n )) + if [ "$x" = "$n" ] + then + shift + update_every=$x + [ $update_every -lt $minimum_update_frequency ] && update_every=$minimum_update_frequency + continue + fi + + echo >&2 "Cannot understand parameter $1. Aborting." + echo "DISABLE" + exit 1 done @@ -157,26 +157,26 @@ done # load my configuration if [ -f "$myconfig" ] - then - . "$myconfig" - if [ $? -ne 0 ] - then - echo >&2 "$PROGRAM_NAME: cannot load $myconfig" - echo "DISABLE" - exit 1 - fi - time_divisor=$((time_divisor)) - [ $time_divisor -lt 10 ] && time_divisor=10 - [ $time_divisor -gt 100 ] && time_divisor=100 + then + . "$myconfig" + if [ $? -ne 0 ] + then + echo >&2 "$PROGRAM_NAME: cannot load $myconfig" + echo "DISABLE" + exit 1 + fi + time_divisor=$((time_divisor)) + [ $time_divisor -lt 10 ] && time_divisor=10 + [ $time_divisor -gt 100 ] && time_divisor=100 else - echo >&2 "$PROGRAM_NAME: configuration file '$myconfig' not found. Using defaults." + echo >&2 "$PROGRAM_NAME: configuration file '$myconfig' not found. Using defaults." fi if [ "$pause_method" = "suspend" ] then - # enable bash job control - # this is required for suspend to work - set -m + # enable bash job control + # this is required for suspend to work + set -m fi # we check for the timeout command, after we load our @@ -185,22 +185,22 @@ fi # can emulate the timeout command we need: # > timeout SECONDS command ... if [ $check_for_timeout -eq 1 ] - then - require_cmd timeout || exit 1 + then + require_cmd timeout || exit 1 fi # ----------------------------------------------------------------------------- # internal checks # netdata passes the requested update frequency as the first argument -update_every=$(( update_every + 1 - 1)) # makes sure it is a number +update_every=$(( update_every + 1 - 1)) # makes sure it is a number test $update_every -eq 0 && update_every=1 # if it is zero, make it 1 # check the charts.d directory if [ ! -d "$chartsd" ] - then - echo >&2 "$PROGRAM_NAME: cannot find charts directory '$chartsd'" - echo "DISABLE" + then + echo >&2 "$PROGRAM_NAME: cannot find charts directory '$chartsd'" + echo "DISABLE" fi @@ -210,13 +210,13 @@ fi # default sleep function LOOPSLEEPMS_HIGHRES=0 loopsleepms() { - [ "$1" = "tellwork" ] && shift - sleep $1 + [ "$1" = "tellwork" ] && shift + sleep $1 } now_ms= current_time_ms() { - now_ms="$(date +'%s')000" + now_ms="$(date +'%s')000" } # if found and included, this file overwrites loopsleepms() @@ -229,10 +229,10 @@ current_time_ms() { # library functions fixid() { - echo "$*" |\ - tr -c "[A-Z][a-z][0-9]" "_" |\ - sed -e "s|^_\+||g" -e "s|_\+$||g" -e "s|_\+|_|g" |\ - tr "[A-Z]" "[a-z]" + echo "$*" |\ + tr -c "[A-Z][a-z][0-9]" "_" |\ + sed -e "s|^_\+||g" -e "s|_\+$||g" -e "s|_\+|_|g" |\ + tr "[A-Z]" "[a-z]" } # convert any floating point number @@ -241,57 +241,57 @@ fixid() { # so that no fork is necessary # the multiplier must be a power of 10 float2int() { - local f m="$2" a b l v=($1) - f=${v[0]} - - # echo >&2 "value='${1}' f='${f}', m='${m}'" - - # the length of the multiplier - 1 - l=$(( ${#m} - 1 )) - - # check if the number is in scientific notation - if [[ ${f} =~ ^[[:space:]]*(-)?[0-9.]+(e|E)(\+|-)[0-9]+ ]] - then - # convert it to decimal - # unfortunately, this fork cannot be avoided - # if you know of a way to avoid it, please let me know - f=$(printf "%0.${l}f" ${f}) - fi - - # split the floating point number - # in integer (a) and decimal (b) - a=${f/.*/} - b=${f/*./} - - # if the integer part is missing - # set it to zero - [ -z "${a}" ] && a="0" - - # strip leading zeros from the integer part - # base 10 convertion - a=$((10#$a)) - - # check the length of the decimal part - # against the length of the multiplier - if [ ${#b} -gt ${l} ] - then - # too many digits - take the most significant - b=${b:0:${l}} - - elif [ ${#b} -lt ${l} ] - then - # too few digits - pad with zero on the right - local z="00000000000000000000000" r=$((l - ${#b})) - b="${b}${z:0:${r}}" - fi - - # strip leading zeros from the decimal part - # base 10 convertion - b=$((10#$b)) - - # store the result - FLOAT2INT_RESULT=$(( (a * m) + b )) - #echo >&2 "FLOAT2INT_RESULT='${FLOAT2INT_RESULT}'" + local f m="$2" a b l v=($1) + f=${v[0]} + + # echo >&2 "value='${1}' f='${f}', m='${m}'" + + # the length of the multiplier - 1 + l=$(( ${#m} - 1 )) + + # check if the number is in scientific notation + if [[ ${f} =~ ^[[:space:]]*(-)?[0-9.]+(e|E)(\+|-)[0-9]+ ]] + then + # convert it to decimal + # unfortunately, this fork cannot be avoided + # if you know of a way to avoid it, please let me know + f=$(printf "%0.${l}f" ${f}) + fi + + # split the floating point number + # in integer (a) and decimal (b) + a=${f/.*/} + b=${f/*./} + + # if the integer part is missing + # set it to zero + [ -z "${a}" ] && a="0" + + # strip leading zeros from the integer part + # base 10 convertion + a=$((10#$a)) + + # check the length of the decimal part + # against the length of the multiplier + if [ ${#b} -gt ${l} ] + then + # too many digits - take the most significant + b=${b:0:${l}} + + elif [ ${#b} -lt ${l} ] + then + # too few digits - pad with zero on the right + local z="00000000000000000000000" r=$((l - ${#b})) + b="${b}${z:0:${r}}" + fi + + # strip leading zeros from the decimal part + # base 10 convertion + b=$((10#$b)) + + # store the result + FLOAT2INT_RESULT=$(( (a * m) + b )) + #echo >&2 "FLOAT2INT_RESULT='${FLOAT2INT_RESULT}'" } @@ -299,84 +299,105 @@ float2int() { # charts check functions all_charts() { - cd "$chartsd" - [ $? -ne 0 ] && echo >&2 "$PROGRAM_NAME: Cannot cd to $chartsd" && return 1 + cd "$chartsd" + [ $? -ne 0 ] && echo >&2 "$PROGRAM_NAME: Cannot cd to $chartsd" && return 1 - ls *.chart.sh | sed "s/\.chart\.sh$//g" + ls *.chart.sh | sed "s/\.chart\.sh$//g" } +declare -A charts_enable_keyword=( + ['apache']="force" + ['cpu_apps']="force" + ['cpufreq']="force" + ['example']="force" + ['exim']="force" + ['hddtemp']="force" + ['load_average']="force" + ['mem_apps']="force" + ['mysql']="force" + ['nginx']="force" + ['phpfpm']="force" + ['postfix']="force" + ['sensors']="force" + ['squid']="force" + ['tomcat']="force" + ) + all_enabled_charts() { - local charts= enabled= - - # find all enabled charts - - for chart in $( all_charts ) - do - eval "enabled=\$$chart" - if [ -z "${enabled}" ] - then - enabled="${enable_all_charts}" - fi - - if [ ! "${enabled}" = "yes" ] - then - echo >&2 "$PROGRAM_NAME: '$chart' is NOT enabled. Add a line with $chart=yes in $myconfig to enable it (or remove the line that disables it)." - else - [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: '$chart' is enabled." - local charts="$charts $chart" - fi - done - - local charts2= - for chart in $charts - do - # check the enabled charts - local check="$( cat "$chartsd/$chart.chart.sh" | sed "s/^ \+//g" | grep "^$chart$charts_check()" )" - if [ -z "$check" ] - then - echo >&2 "$PROGRAM_NAME: chart '$chart' does not seem to have a $chart$charts_check() function. Disabling it." - continue - fi - - local create="$( cat "$chartsd/$chart.chart.sh" | sed "s/^ \+//g" | grep "^$chart$charts_create()" )" - if [ -z "$create" ] - then - echo >&2 "$PROGRAM_NAME: chart '$chart' does not seem to have a $chart$charts_create() function. Disabling it." - continue - fi - - local update="$( cat "$chartsd/$chart.chart.sh" | sed "s/^ \+//g" | grep "^$chart$charts_update()" )" - if [ -z "$update" ] - then - echo >&2 "$PROGRAM_NAME: chart '$chart' does not seem to have a $chart$charts_update() function. Disabling it." - continue - fi - - # check its config - #if [ -f "$confd/$chart.conf" ] - #then - # if [ ! -z "$( cat "$confd/$chart.conf" | sed "s/^ \+//g" | grep -v "^$" | grep -v "^#" | grep -v "^$chart$charts_undescore" )" ] - # then - # echo >&2 "$PROGRAM_NAME: chart's $chart config $confd/$chart.conf should only have lines starting with $chart$charts_undescore . Disabling it." - # continue - # fi - #fi - - #if [ $dryrunner -eq 1 ] - # then - # "$pluginsd/charts.d.dryrun-helper.sh" "$chart" "$chartsd/$chart.chart.sh" "$confd/$chart.conf" >/dev/null - # if [ $? -ne 0 ] - # then - # 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." - # continue - # fi - #fi - - local charts2="$charts2 $chart" - done - - echo $charts2 - [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: enabled charts: $charts2" + local charts= enabled= required= + + # find all enabled charts + + for chart in $( all_charts ) + do + eval "enabled=\$$chart" + if [ -z "${enabled}" ] + then + enabled="${enable_all_charts}" + fi + + required="${charts_enable_keyword[${chart}]}" + [ -z "${required}" ] && required="yes" + + if [ ! "${enabled}" = "${required}" ] + then + 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)." + else + [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: '$chart' is enabled." + local charts="$charts $chart" + fi + done + + local charts2= + for chart in $charts + do + # check the enabled charts + local check="$( cat "$chartsd/$chart.chart.sh" | sed "s/^ \+//g" | grep "^$chart$charts_check()" )" + if [ -z "$check" ] + then + echo >&2 "$PROGRAM_NAME: chart '$chart' does not seem to have a $chart$charts_check() function. Disabling it." + continue + fi + + local create="$( cat "$chartsd/$chart.chart.sh" | sed "s/^ \+//g" | grep "^$chart$charts_create()" )" + if [ -z "$create" ] + then + echo >&2 "$PROGRAM_NAME: chart '$chart' does not seem to have a $chart$charts_create() function. Disabling it." + continue + fi + + local update="$( cat "$chartsd/$chart.chart.sh" | sed "s/^ \+//g" | grep "^$chart$charts_update()" )" + if [ -z "$update" ] + then + echo >&2 "$PROGRAM_NAME: chart '$chart' does not seem to have a $chart$charts_update() function. Disabling it." + continue + fi + + # check its config + #if [ -f "$confd/$chart.conf" ] + #then + # if [ ! -z "$( cat "$confd/$chart.conf" | sed "s/^ \+//g" | grep -v "^$" | grep -v "^#" | grep -v "^$chart$charts_undescore" )" ] + # then + # echo >&2 "$PROGRAM_NAME: chart's $chart config $confd/$chart.conf should only have lines starting with $chart$charts_undescore . Disabling it." + # continue + # fi + #fi + + #if [ $dryrunner -eq 1 ] + # then + # "$pluginsd/charts.d.dryrun-helper.sh" "$chart" "$chartsd/$chart.chart.sh" "$confd/$chart.conf" >/dev/null + # if [ $? -ne 0 ] + # then + # 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." + # continue + # fi + #fi + + local charts2="$charts2 $chart" + done + + echo $charts2 + [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: enabled charts: $charts2" } @@ -387,32 +408,36 @@ suffix_update_every="_update_every" active_charts= for chart in $( all_enabled_charts ) do - [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: loading chart: '$chartsd/$chart.chart.sh'" - . "$chartsd/$chart.chart.sh" - - if [ -f "$confd/$chart.conf" ] - then - [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: loading chart options: '$confd/$chart.conf'" - . "$confd/$chart.conf" - else - echo >&2 "$PROGRAM_NAME: $chart: configuration file '$confd/$chart.conf' not found. Using defaults." - fi - - eval "dt=\$$chart$suffix_update_every" - dt=$(( dt + 1 - 1 )) # make sure it is a number - if [ $dt -lt $update_every ] - then - eval "$chart$suffix_update_every=$update_every" - fi - - $chart$charts_check - if [ $? -eq 0 ] - then - [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: '$chart' activated" - active_charts="$active_charts $chart" - else - echo >&2 "$PROGRAM_NAME: chart '$chart' check() function reports failure." - fi + [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: loading chart: '$chartsd/$chart.chart.sh'" + . "$chartsd/$chart.chart.sh" + + if [ -f "$confd/charts.d/$chart.conf" ] + then + [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: loading chart options: '$confd/charts.d/$chart.conf'" + . "$confd/charts.d/$chart.conf" + elif [ -f "$confd/$chart.conf" ] + then + [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: loading chart options: '$confd/$chart.conf'" + . "$confd/$chart.conf" + else + echo >&2 "$PROGRAM_NAME: $chart: configuration file '$confd/charts.d/$chart.conf' not found. Using defaults." + fi + + eval "dt=\$$chart$suffix_update_every" + dt=$(( dt + 1 - 1 )) # make sure it is a number + if [ $dt -lt $update_every ] + then + eval "$chart$suffix_update_every=$update_every" + fi + + $chart$charts_check + if [ $? -eq 0 ] + then + [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: '$chart' activated" + active_charts="$active_charts $chart" + else + echo >&2 "$PROGRAM_NAME: chart '$chart' check() function reports failure." + fi done [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: activated charts: $active_charts" @@ -427,26 +452,26 @@ test $debug -eq 1 && debug_time=tellwork # if we only need a specific chart, remove all the others if [ ! -z "${chart_only}" ] then - [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: requested to run only for: '${chart_only}'" - check_charts= - for chart in $active_charts - do - if [ "$chart" = "$chart_only" ] - then - check_charts="$chart" - break - fi - done - active_charts="$check_charts" + [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: requested to run only for: '${chart_only}'" + check_charts= + for chart in $active_charts + do + if [ "$chart" = "$chart_only" ] + then + check_charts="$chart" + break + fi + done + active_charts="$check_charts" fi [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: activated charts: $active_charts" # stop if we just need a pre-check if [ $check -eq 1 ] then - echo >&2 "CHECK RESULT" - echo >&2 "Will run the charts: $active_charts" - exit 0 + echo >&2 "CHECK RESULT" + echo >&2 "Will run the charts: $active_charts" + exit 0 fi # ----------------------------------------------------------------------------- @@ -454,13 +479,13 @@ fi TMP_DIR= chartsd_cleanup() { - cd /tmp - if [ ! -z "$TMP_DIR" -a -d "$TMP_DIR" ] - then - [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: cleaning up temporary directory $TMP_DIR ..." - rm -rf "$TMP_DIR" - fi - exit 0 + cd /tmp + if [ ! -z "$TMP_DIR" -a -d "$TMP_DIR" ] + then + [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: cleaning up temporary directory $TMP_DIR ..." + rm -rf "$TMP_DIR" + fi + exit 0 } trap chartsd_cleanup EXIT trap chartsd_cleanup SIGHUP @@ -468,9 +493,9 @@ trap chartsd_cleanup INT if [ $UID = "0" ] then - TMP_DIR="$( mktemp -d /var/run/netdata-${PROGRAM_NAME}-XXXXXXXXXX )" + TMP_DIR="$( mktemp -d /var/run/netdata-${PROGRAM_NAME}-XXXXXXXXXX )" else - TMP_DIR="$( mktemp -d /tmp/.netdata-${PROGRAM_NAME}-XXXXXXXXXX )" + TMP_DIR="$( mktemp -d /tmp/.netdata-${PROGRAM_NAME}-XXXXXXXXXX )" fi cd "$TMP_DIR" || exit 1 @@ -481,15 +506,15 @@ cd "$TMP_DIR" || exit 1 run_charts= for chart in $active_charts do - [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: Calling '$chart$charts_create()'..." - $chart$charts_create - if [ $? -eq 0 ] - then - run_charts="$run_charts $chart" - [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: '$chart' has initialized." - else - echo >&2 "$PROGRAM_NAME: chart '$chart' function '$chart$charts_create()' reports failure." - fi + [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: Calling '$chart$charts_create()'..." + $chart$charts_create + if [ $? -eq 0 ] + then + run_charts="$run_charts $chart" + [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: '$chart' has initialized." + else + echo >&2 "$PROGRAM_NAME: chart '$chart' function '$chart$charts_create()' reports failure." + fi done [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: run_charts='$run_charts'" @@ -498,109 +523,131 @@ done # update dimensions if [ -z "$run_charts" ] - then - echo >&2 "$PROGRAM_NAME: No charts to collect data from." - echo "DISABLE" - exit 1 + then + echo >&2 "$PROGRAM_NAME: No charts to collect data from." + echo "DISABLE" + exit 1 fi -declare -A charts_last_update=() charts_update_every=() charts_next_update=() charts_run_counter=() +declare -A charts_last_update=() charts_update_every=() charts_next_update=() charts_run_counter=() charts_serial_failures=() global_update() { - local exit_at \ - c=0 dt ret last_ms exec_start_ms exec_end_ms \ - chart now_charts=() next_charts=($run_charts) - - # return the current time in ms in $now_ms - current_time_ms - - exit_at=$(( now_ms + (restart_timeout * 1000) )) - - for chart in $run_charts - do - eval "charts_update_every[$chart]=\$$chart$suffix_update_every" - test -z "${charts_update_every[$chart]}" && charts_update_every[$charts]=$update_every - charts_last_update[$chart]=$((now_ms - (now_ms % (charts_update_every[$chart] * 1000) ) )) - charts_next_update[$chart]=$(( charts_last_update[$chart] + (charts_update_every[$chart] * 1000) )) - charts_run_counter[$chart]=0 - - echo "CHART netdata.plugin_chartsd_$chart '' 'Execution time for $chart plugin' 'milliseconds / run' charts.d netdata.plugin_charts area 145000 ${charts_update_every[$chart]}" - echo "DIMENSION run_time 'run time' absolute 1 1" - done - - # the main loop - while [ 1 ] - do - c=$((c + 1)) - now_charts=("${next_charts[@]}") - next_charts=() - - # return the current time in ms in $now_ms - current_time_ms - - for chart in "${now_charts[@]}" - do - # echo >&2 "DEBUG: chart: $chart last: ${charts_last_update[$chart]}, next: ${charts_next_update[$chart]}, now: ${now_ms}" - if [ ${now_ms} -ge ${charts_next_update[$chart]} ] - then - last_ms=${charts_last_update[$chart]} - dt=$(( (now_ms - last_ms) )) - # echo >&2 "DEBUG: chart: $chart last: ${charts_last_update[$chart]}, next: ${charts_next_update[$chart]}, now: ${now_ms}, dt: ${dt}" - - charts_last_update[$chart]=${now_ms} - - while [ ${charts_next_update[$chart]} -lt ${now_ms} ] - do - charts_next_update[$chart]=$(( charts_next_update[$chart] + (charts_update_every[$chart] * 1000) )) - done - - # the first call should not give a duration - # so that netdata calibrates to current time - dt=$(( dt * 1000 )) - charts_run_counter[$chart]=$(( charts_run_counter[$chart] + 1 )) - if [ ${charts_run_counter[$chart]} -eq 1 ] - then - dt= - fi - - exec_start_ms=$now_ms - $chart$charts_update $dt - ret=$? - - # return the current time in ms in $now_ms - current_time_ms; exec_end_ms=$now_ms - - echo "BEGIN netdata.plugin_chartsd_$chart $dt" - if [ $ret -eq 0 ] - then - echo "SET run_time = $(( exec_end_ms - exec_start_ms ))" - next_charts+=($chart) - else - echo "SET run_time = $(( (exec_end_ms - exec_start_ms) * -1 ))" - echo >&2 "$PROGRAM_NAME: chart '$chart' update() function reports failure. Disabling it." - fi - echo "END" - else - next_charts+=($chart) - fi - done - - if [ "$pause_method" = "suspend" ] - then - echo "STOPPING_WAKE_ME_UP_PLEASE" - suspend || ( echo >&2 "$PROGRAM_NAME: suspend returned error $?, falling back to sleep."; loopsleepms $debug_time $update_every $time_divisor) - else - # wait the time you are required to - #loopsleepms $debug_time $update_every $time_divisor - if [ ${LOOPSLEEPMS_HIGHRES} -eq 1 ] - then - sleep 0.2 - else - sleep 1 - fi - fi - - test ${now_ms} -ge ${exit_at} && exit 0 - done + local exit_at \ + c=0 dt ret last_ms exec_start_ms exec_end_ms \ + chart now_charts=() next_charts=($run_charts) \ + next_ms x seconds millis + + # return the current time in ms in $now_ms + current_time_ms + + exit_at=$(( now_ms + (restart_timeout * 1000) )) + + for chart in $run_charts + do + eval "charts_update_every[$chart]=\$$chart$suffix_update_every" + test -z "${charts_update_every[$chart]}" && charts_update_every[$charts]=$update_every + charts_last_update[$chart]=$((now_ms - (now_ms % (charts_update_every[$chart] * 1000) ) )) + charts_next_update[$chart]=$(( charts_last_update[$chart] + (charts_update_every[$chart] * 1000) )) + charts_run_counter[$chart]=0 + charts_serial_failures[$chart]=0 + + echo "CHART netdata.plugin_chartsd_$chart '' 'Execution time for $chart plugin' 'milliseconds / run' charts.d netdata.plugin_charts area 145000 ${charts_update_every[$chart]}" + echo "DIMENSION run_time 'run time' absolute 1 1" + done + + # the main loop + while [ "${#next_charts[@]}" -gt 0 ] + do + c=$((c + 1)) + now_charts=("${next_charts[@]}") + next_charts=() + + # return the current time in ms in $now_ms + current_time_ms + + for chart in "${now_charts[@]}" + do + # echo >&2 "DEBUG: chart: $chart last: ${charts_last_update[$chart]}, next: ${charts_next_update[$chart]}, now: ${now_ms}" + if [ ${now_ms} -ge ${charts_next_update[$chart]} ] + then + last_ms=${charts_last_update[$chart]} + dt=$(( (now_ms - last_ms) )) + # echo >&2 "DEBUG: chart: $chart last: ${charts_last_update[$chart]}, next: ${charts_next_update[$chart]}, now: ${now_ms}, dt: ${dt}" + + charts_last_update[$chart]=${now_ms} + + while [ ${charts_next_update[$chart]} -lt ${now_ms} ] + do + charts_next_update[$chart]=$(( charts_next_update[$chart] + (charts_update_every[$chart] * 1000) )) + done + + # the first call should not give a duration + # so that netdata calibrates to current time + dt=$(( dt * 1000 )) + charts_run_counter[$chart]=$(( charts_run_counter[$chart] + 1 )) + if [ ${charts_run_counter[$chart]} -eq 1 ] + then + dt= + fi + + exec_start_ms=$now_ms + $chart$charts_update $dt + ret=$? + + # return the current time in ms in $now_ms + current_time_ms; exec_end_ms=$now_ms + + echo "BEGIN netdata.plugin_chartsd_$chart $dt" + echo "SET run_time = $(( exec_end_ms - exec_start_ms ))" + echo "END" + + if [ $ret -eq 0 ] + then + charts_serial_failures[$chart]=0 + next_charts+=($chart) + else + charts_serial_failures[$chart]=$(( charts_serial_failures[$chart] + 1 )) + + if [ ${charts_serial_failures[$chart]} -gt 10 ] + then + echo >&2 "$PROGRAM_NAME: chart '$chart' update() function reported failure ${charts_serial_failures[$chart]} times. Disabling it." + else + echo >&2 "$PROGRAM_NAME: chart '$chart' update() function reports failure. Will keep trying for a while." + next_charts+=($chart) + fi + fi + else + next_charts+=($chart) + fi + done + + if [ "$pause_method" = "suspend" ] + then + echo "STOPPING_WAKE_ME_UP_PLEASE" + suspend || ( echo >&2 "$PROGRAM_NAME: suspend returned error $?, falling back to sleep."; loopsleepms $debug_time $update_every $time_divisor) + else + # wait the time you are required to + next_ms=$((now_ms + (update_every * 1000 * 100) )) + for x in "${charts_next_update[@]}"; do [ ${x} -lt ${next_ms} ] && next_ms=${x}; done + next_ms=$((next_ms - now_ms)) + + if [ ${LOOPSLEEPMS_HIGHRES} -eq 1 -a ${next_ms} -gt 0 ] + then + seconds=$(( next_ms / 1000 )) + millis=$(( next_ms % 1000 )) + [ ${millis} -lt 10 ] && millis="0${millis}" + [ ${millis} -lt 100 ] && millis="0${millis}" + [ $debug -eq 1 ] && echo >&2 "$PROGRAM_NAME: sleeping for ${seconds}.${millis} seconds." + sleep ${seconds}.${millis} + else + sleep $update_every + fi + fi + + test ${now_ms} -ge ${exit_at} && exit 0 + done + + echo >&2 "$PROGRAM_NAME: Nothing left to do. Disabling charts.d.plugin." + echo "DISABLE" } global_update diff --git a/plugins.d/loopsleepms.sh.inc b/plugins.d/loopsleepms.sh.inc index 2e22de3d8..02ab694d2 100644 --- a/plugins.d/loopsleepms.sh.inc +++ b/plugins.d/loopsleepms.sh.inc @@ -1,4 +1,4 @@ -#!/bin/bash +# no need for shebang - this file is included from other scripts # this function is used to sleep a fraction of a second # it calculates the difference between every time is called @@ -7,9 +7,9 @@ LOOPSLEEP_DATE="$(which date)" if [ -z "$LOOPSLEEP_DATE" ] - then - echo >&2 "$0: ERROR: Cannot find the command 'date' in the system path." - exit 1 + then + echo >&2 "$0: ERROR: Cannot find the command 'date' in the system path." + exit 1 fi LOOPSLEEPMS_LASTRUN=0 @@ -21,70 +21,70 @@ test "$($LOOPSLEEP_DATE +%N)" = "%N" && LOOPSLEEPMS_HIGHRES=0 now_ms= current_time_ms() { - # if high resolution is not supported - # just sleep the time requested, in seconds - if [ $LOOPSLEEPMS_HIGHRES -eq 0 ] - then - now_ms="$($LOOPSLEEP_DATE +'%s')000" - else - now_ms="$(( $( $LOOPSLEEP_DATE +'%s * 1000 + %-N / 1000000' ) ))" - fi + # if high resolution is not supported + # just sleep the time requested, in seconds + if [ $LOOPSLEEPMS_HIGHRES -eq 0 ] + then + now_ms="$($LOOPSLEEP_DATE +'%s')000" + else + now_ms="$(( $( $LOOPSLEEP_DATE +'%s * 1000 + %-N / 1000000' ) ))" + fi } loopsleepms() { - local tellwork=0 t="$1" div s m now mstosleep + local tellwork=0 t="$1" div s m now mstosleep - if [ "$t" = "tellwork" ] - then - tellwork=1 - shift - t="$1" - fi - div="${2-100}" + if [ "$t" = "tellwork" ] + then + tellwork=1 + shift + t="$1" + fi + div="${2-100}" - # $t = the time in seconds to wait + # $t = the time in seconds to wait - # if high resolution is not supported - # just sleep the time requested, in seconds - if [ $LOOPSLEEPMS_HIGHRES -eq 0 ] - then - sleep $t - return - fi + # if high resolution is not supported + # just sleep the time requested, in seconds + if [ $LOOPSLEEPMS_HIGHRES -eq 0 ] + then + sleep $t + return + fi - # get the current time, in ms - # milliseconds since epoch (1-1-1970) - now="$(( $( $LOOPSLEEP_DATE +'%s * 1000 + %-N / 1000000' ) ))" + # get the current time, in ms + # milliseconds since epoch (1-1-1970) + now="$(( $( $LOOPSLEEP_DATE +'%s * 1000 + %-N / 1000000' ) ))" - # calculate required sleep in ms - t=$((t * 1000 * div / 100)) + # calculate required sleep in ms + t=$((t * 1000 * div / 100)) - # this is our first run - # just wait the requested time - test $LOOPSLEEPMS_LASTRUN -eq 0 && LOOPSLEEPMS_LASTRUN=$now + # this is our first run + # just wait the requested time + test $LOOPSLEEPMS_LASTRUN -eq 0 && LOOPSLEEPMS_LASTRUN=$now - # calculate ms since last run - LOOPSLEEPMS_LASTWORK=$((now - LOOPSLEEPMS_LASTRUN - LOOPSLEEPMS_LASTSLEEP)) - # echo "# last loop's work took $LOOPSLEEPMS_LASTWORK ms" + # calculate ms since last run + LOOPSLEEPMS_LASTWORK=$((now - LOOPSLEEPMS_LASTRUN - LOOPSLEEPMS_LASTSLEEP)) + # echo "# last loop's work took $LOOPSLEEPMS_LASTWORK ms" - # calculate ms to sleep - mstosleep=$(( t - LOOPSLEEPMS_LASTWORK )) - # echo "# mstosleep is $mstosleep ms" + # calculate ms to sleep + mstosleep=$(( t - LOOPSLEEPMS_LASTWORK )) + # echo "# mstosleep is $mstosleep ms" - # if we are too slow, sleep some time - test $mstosleep -lt 200 && mstosleep=200 + # if we are too slow, sleep some time + test $mstosleep -lt 200 && mstosleep=200 - s=$((mstosleep / 1000)) - m=$((mstosleep - (s * 1000))) + s=$((mstosleep / 1000)) + m=$((mstosleep - (s * 1000))) - test $tellwork -eq 1 && echo >&2 " >>> PERFORMANCE >>> WORK TOOK $LOOPSLEEPMS_LASTWORK ms ( $((LOOPSLEEPMS_LASTWORK * 100 / 1000)).$((LOOPSLEEPMS_LASTWORK % 10))% cpu ) >>> SLEEPING $mstosleep ms" + test $tellwork -eq 1 && echo >&2 " >>> PERFORMANCE >>> WORK TOOK $LOOPSLEEPMS_LASTWORK ms ( $((LOOPSLEEPMS_LASTWORK * 100 / 1000)).$((LOOPSLEEPMS_LASTWORK % 10))% cpu ) >>> SLEEPING $mstosleep ms" - # echo "# sleeping $s.$m" - # echo - sleep $s.$m + # echo "# sleeping $s.$m" + # echo + sleep $s.$m - # keep the values we need - # for our next run - LOOPSLEEPMS_LASTRUN=$now - LOOPSLEEPMS_LASTSLEEP=$mstosleep + # keep the values we need + # for our next run + LOOPSLEEPMS_LASTRUN=$now + LOOPSLEEPMS_LASTSLEEP=$mstosleep } diff --git a/plugins.d/node.d.plugin b/plugins.d/node.d.plugin index a1fa754fa..21b04384e 100755 --- a/plugins.d/node.d.plugin +++ b/plugins.d/node.d.plugin @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash ':' //; exec "$(command -v nodejs || command -v node || command -v js || echo "ERROR node.js IS NOT AVAILABLE IN THIS SYSTEM")" "$0" "$@" // shebang hack from: @@ -40,55 +40,60 @@ var netdata = require('netdata'); // configuration function pluginConfig(filename) { - var f = path.basename(filename); + var f = path.basename(filename); - var m = f.match('.plugin' + '$'); - if(m === null) m = f.match('.node.js' + '$'); - if(m !== null) - return netdata.options.paths.config + '/' + f.substring(0, m.index) + '.conf'; + // node.d.plugin configuration + var m = f.match('.plugin' + '$'); + if(m !== null) + return netdata.options.paths.config + '/' + f.substring(0, m.index) + '.conf'; - return netdata.options.paths.config + '/' + f + '.conf'; + // node.d modules configuration + m = f.match('.node.js' + '$'); + if(m !== null) + return netdata.options.paths.config + '/node.d/' + f.substring(0, m.index) + '.conf'; + + return netdata.options.paths.config + '/node.d/' + f + '.conf'; } // internal defaults extend(true, netdata.options, { - filename: path.basename(__filename), + filename: path.basename(__filename), - update_every: NETDATA_UPDATE_EVERY, + update_every: NETDATA_UPDATE_EVERY, - exit_after_ms: 3600 * 4 * 1000, + exit_after_ms: 3600 * 4 * 1000, - paths: { - plugins: NETDATA_PLUGINS_DIR, - config: NETDATA_CONFIG_DIR, - modules: [], - }, + paths: { + plugins: NETDATA_PLUGINS_DIR, + config: NETDATA_CONFIG_DIR, + modules: [], + }, - modules_enable_autodetect: true, - modules_enable_all: true, - modules: {}, + modules_enable_autodetect: true, + modules_enable_all: true, + modules: {}, }); netdata.options.config_filename = pluginConfig(__filename); // load configuration file try { - netdata.options_loaded = JSON.parse(fs.readFileSync(netdata.options.config_filename, 'utf8')); - extend(true, netdata.options, netdata.options_loaded); - console.error('merged netdata object:'); - console.error(netdata); + netdata.options_loaded = JSON.parse(fs.readFileSync(netdata.options.config_filename, 'utf8')); + extend(true, netdata.options, netdata.options_loaded); + console.error('merged netdata object:'); + console.error(netdata); } catch(e) { - netdata.error('Cannot read configuration file ' + netdata.options.config_filename + ': ' + e.message + ', using internal defaults.'); - netdata.options_loaded = undefined; - dumpError(e); + netdata.error('Cannot read configuration file ' + netdata.options.config_filename + ': ' + e.message + ', using internal defaults.'); + netdata.options_loaded = undefined; + dumpError(e); } // apply module paths to node.js process function applyModulePaths() { - var len = netdata.options.paths.modules.length; - while(len--) - process.mainModule.paths.unshift(netdata.options.paths.modules[len]); + var len = netdata.options.paths.modules.length; + while(len--) + process.mainModule.paths.unshift(netdata.options.paths.modules[len]); } applyModulePaths(); @@ -97,165 +102,165 @@ applyModulePaths(); // tracing function dumpError(err) { - if (typeof err === 'object') { - if (err.stack) { - netdata.debug(err.stack); - } - } + if (typeof err === 'object') { + if (err.stack) { + netdata.debug(err.stack); + } + } } // -------------------------------------------------------------------------------------------------------------------- // get command line arguments { - var found_myself = false; - var found_number = false; - var found_modules = false; - process.argv.forEach(function (val, index, array) { - netdata.debug('PARAM: ' + val); - - if(!found_myself) { - if(val === __filename) - found_myself = true; - } - else { - switch(val) { - case 'debug': - netdata.options.DEBUG = true; - netdata.debug('DEBUG enabled'); - break; - - default: - if(found_number === true) { - if(found_modules === false) { - for(var i in netdata.options.modules) - netdata.options.modules[i].enabled = false; - } - - if(typeof netdata.options.modules[val] === 'undefined') - netdata.options.modules[val] = {}; - - netdata.options.modules[val].enabled = true; - netdata.options.modules_enable_all = false; - netdata.debug('enabled module ' + val); - } - else { - try { - var x = parseInt(val); - if(x > 0) { - netdata.options.update_every = x; - if(netdata.options.update_every < NETDATA_UPDATE_EVERY) { - netdata.options.update_every = NETDATA_UPDATE_EVERY; - netdata.debug('Update frequency ' + x + 's is too low'); - } - - found_number = true; - netdata.debug('Update frequency set to ' + netdata.options.update_every + ' seconds'); - } - else netdata.error('Ignoring parameter: ' + val); - } - catch(e) { - netdata.error('Cannot get value of parameter: ' + val); - dumpError(e); - } - } - break; - } - } - }); + var found_myself = false; + var found_number = false; + var found_modules = false; + process.argv.forEach(function (val, index, array) { + netdata.debug('PARAM: ' + val); + + if(!found_myself) { + if(val === __filename) + found_myself = true; + } + else { + switch(val) { + case 'debug': + netdata.options.DEBUG = true; + netdata.debug('DEBUG enabled'); + break; + + default: + if(found_number === true) { + if(found_modules === false) { + for(var i in netdata.options.modules) + netdata.options.modules[i].enabled = false; + } + + if(typeof netdata.options.modules[val] === 'undefined') + netdata.options.modules[val] = {}; + + netdata.options.modules[val].enabled = true; + netdata.options.modules_enable_all = false; + netdata.debug('enabled module ' + val); + } + else { + try { + var x = parseInt(val); + if(x > 0) { + netdata.options.update_every = x; + if(netdata.options.update_every < NETDATA_UPDATE_EVERY) { + netdata.options.update_every = NETDATA_UPDATE_EVERY; + netdata.debug('Update frequency ' + x + 's is too low'); + } + + found_number = true; + netdata.debug('Update frequency set to ' + netdata.options.update_every + ' seconds'); + } + else netdata.error('Ignoring parameter: ' + val); + } + catch(e) { + netdata.error('Cannot get value of parameter: ' + val); + dumpError(e); + } + } + break; + } + } + }); } if(netdata.options.update_every < 1) { - netdata.debug('Adjusting update frequency to 1 second'); - netdata.options.update_every = 1; + netdata.debug('Adjusting update frequency to 1 second'); + netdata.options.update_every = 1; } // -------------------------------------------------------------------------------------------------------------------- // find modules function findModules() { - var found = 0; - - var files = fs.readdirSync(NODE_D_DIR); - var len = files.length; - while(len--) { - var m = files[len].match('.node.js' + '$'); - if(m !== null) { - var n = files[len].substring(0, m.index); - - if(typeof(netdata.options.modules[n]) === 'undefined') - netdata.options.modules[n] = { name: n, enabled: netdata.options.modules_enable_all }; - - if(netdata.options.modules[n].enabled === true) { - netdata.options.modules[n].name = n; - netdata.options.modules[n].filename = NODE_D_DIR + '/' + files[len]; - netdata.options.modules[n].loaded = false; - - if(typeof(netdata.options.modules[n].config_filename) !== 'string') - netdata.options.modules[n].config_filename = pluginConfig(files[len]); - - // load the module - try { - netdata.debug('loading module ' + netdata.options.modules[n].filename); - netdata.options.modules[n].module = require(netdata.options.modules[n].filename); - netdata.options.modules[n].module.name = n; - netdata.debug('loaded module ' + netdata.options.modules[n].name + ' from ' + netdata.options.modules[n].filename); - } - catch(e) { - netdata.options.modules[n].enabled = false; - netdata.error('Cannot load module: ' + netdata.options.modules[n].filename + ' exception: ' + e); - dumpError(e); - continue; - } - - // load its configuration - var c = { - enable_autodetect: netdata.options.modules_enable_autodetect, - update_every: netdata.options.update_every - }; - try { - netdata.debug('loading module\'s ' + netdata.options.modules[n].name + ' config ' + netdata.options.modules[n].config_filename); - var c2 = JSON.parse(fs.readFileSync(netdata.options.modules[n].config_filename, 'utf8')); - extend(true, c, c2); - netdata.debug('loaded module\'s ' + netdata.options.modules[n].name + ' config ' + netdata.options.modules[n].config_filename); - } - catch(e) { - netdata.error('Cannot load module\'s ' + netdata.options.modules[n].name + ' config from ' + netdata.options.modules[n].config_filename + ' exception: ' + e + ', using internal defaults.'); - dumpError(e); - } - - // call module auto-detection / configuration - try { - netdata.modules_configuring++; - netdata.debug('Configuring module ' + netdata.options.modules[n].name); - var serv = netdata.configure(netdata.options.modules[n].module, c, function() { - netdata.debug('Configured module ' + netdata.options.modules[n].name); - netdata.modules_configuring--; - }); - - netdata.debug('Configuring module ' + netdata.options.modules[n].name + ' reports ' + serv + ' eligible services.'); - } - catch(e) { - netdata.modules_configuring--; - netdata.options.modules[n].enabled = false; - netdata.error('Failed module auto-detection: ' + netdata.options.modules[n].name + ' exception: ' + e + ', disabling module.'); - dumpError(e); - continue; - } - - netdata.options.modules[n].loaded = true; - found++; - } - } - } - - // netdata.debug(netdata.options.modules); - return found; + var found = 0; + + var files = fs.readdirSync(NODE_D_DIR); + var len = files.length; + while(len--) { + var m = files[len].match('.node.js' + '$'); + if(m !== null) { + var n = files[len].substring(0, m.index); + + if(typeof(netdata.options.modules[n]) === 'undefined') + netdata.options.modules[n] = { name: n, enabled: netdata.options.modules_enable_all }; + + if(netdata.options.modules[n].enabled === true) { + netdata.options.modules[n].name = n; + netdata.options.modules[n].filename = NODE_D_DIR + '/' + files[len]; + netdata.options.modules[n].loaded = false; + + if(typeof(netdata.options.modules[n].config_filename) !== 'string') + netdata.options.modules[n].config_filename = pluginConfig(files[len]); + + // load the module + try { + netdata.debug('loading module ' + netdata.options.modules[n].filename); + netdata.options.modules[n].module = require(netdata.options.modules[n].filename); + netdata.options.modules[n].module.name = n; + netdata.debug('loaded module ' + netdata.options.modules[n].name + ' from ' + netdata.options.modules[n].filename); + } + catch(e) { + netdata.options.modules[n].enabled = false; + netdata.error('Cannot load module: ' + netdata.options.modules[n].filename + ' exception: ' + e); + dumpError(e); + continue; + } + + // load its configuration + var c = { + enable_autodetect: netdata.options.modules_enable_autodetect, + update_every: netdata.options.update_every + }; + try { + netdata.debug('loading module\'s ' + netdata.options.modules[n].name + ' config ' + netdata.options.modules[n].config_filename); + var c2 = JSON.parse(fs.readFileSync(netdata.options.modules[n].config_filename, 'utf8')); + extend(true, c, c2); + netdata.debug('loaded module\'s ' + netdata.options.modules[n].name + ' config ' + netdata.options.modules[n].config_filename); + } + catch(e) { + netdata.error('Cannot load module\'s ' + netdata.options.modules[n].name + ' config from ' + netdata.options.modules[n].config_filename + ' exception: ' + e + ', using internal defaults.'); + dumpError(e); + } + + // call module auto-detection / configuration + try { + netdata.modules_configuring++; + netdata.debug('Configuring module ' + netdata.options.modules[n].name); + var serv = netdata.configure(netdata.options.modules[n].module, c, function() { + netdata.debug('Configured module ' + netdata.options.modules[n].name); + netdata.modules_configuring--; + }); + + netdata.debug('Configuring module ' + netdata.options.modules[n].name + ' reports ' + serv + ' eligible services.'); + } + catch(e) { + netdata.modules_configuring--; + netdata.options.modules[n].enabled = false; + netdata.error('Failed module auto-detection: ' + netdata.options.modules[n].name + ' exception: ' + e + ', disabling module.'); + dumpError(e); + continue; + } + + netdata.options.modules[n].loaded = true; + found++; + } + } + } + + // netdata.debug(netdata.options.modules); + return found; } if(findModules() === 0) { - netdata.error('Cannot load any .node.js module from: ' + NODE_D_DIR); - netdata.disableNodePlugin(); - process.exit(1); + netdata.error('Cannot load any .node.js module from: ' + NODE_D_DIR); + netdata.disableNodePlugin(); + process.exit(1); } @@ -263,14 +268,14 @@ if(findModules() === 0) { // start function start_when_configuring_ends() { - if(netdata.modules_configuring > 0) { - netdata.debug('Waiting modules configuration, still running ' + netdata.modules_configuring); - setTimeout(start_when_configuring_ends, 500); - return; - } - - netdata.modules_configuring = 0; - netdata.start(); + if(netdata.modules_configuring > 0) { + netdata.debug('Waiting modules configuration, still running ' + netdata.modules_configuring); + setTimeout(start_when_configuring_ends, 500); + return; + } + + netdata.modules_configuring = 0; + netdata.start(); } start_when_configuring_ends(); diff --git a/plugins.d/python.d.plugin b/plugins.d/python.d.plugin new file mode 100755 index 000000000..5e81fb263 --- /dev/null +++ b/plugins.d/python.d.plugin @@ -0,0 +1,533 @@ +#!/usr/bin/env bash +'''':; exec "$(command -v python || command -v python3 || command -v python2 || echo "ERROR python IS NOT AVAILABLE IN THIS SYSTEM")" "$0" "$@" # ''' +# -*- coding: utf-8 -*- + +# Description: netdata python modules supervisor +# Author: Pawel Krupa (paulfantom) + +import os +import sys +import time +import threading + +# ----------------------------------------------------------------------------- +# globals & environment setup +# https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables +MODULE_EXTENSION = ".chart.py" +BASE_CONFIG = {'update_every': os.getenv('NETDATA_UPDATE_EVERY', 1), + 'priority': 90000, + 'retries': 10} + +MODULES_DIR = os.path.abspath(os.getenv('NETDATA_PLUGINS_DIR', + os.path.dirname(__file__)) + "/../python.d") + "/" +CONFIG_DIR = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/") +# directories should end with '/' +if CONFIG_DIR[-1] != "/": + CONFIG_DIR += "/" +sys.path.append(MODULES_DIR + "python_modules") + +PROGRAM = os.path.basename(__file__).replace(".plugin", "") +DEBUG_FLAG = False +OVERRIDE_UPDATE_EVERY = False + +# ----------------------------------------------------------------------------- +# custom, third party and version specific python modules management +import msg + +try: + assert sys.version_info >= (3, 1) + import importlib.machinery + PY_VERSION = 3 + # change this hack below if we want PY_VERSION to be used in modules + # import builtins + # builtins.PY_VERSION = 3 + msg.info('Using python v3') +except (AssertionError, ImportError): + try: + import imp + + # change this hack below if we want PY_VERSION to be used in modules + # import __builtin__ + # __builtin__.PY_VERSION = 2 + PY_VERSION = 2 + msg.info('Using python v2') + except ImportError: + msg.fatal('Cannot start. No importlib.machinery on python3 or lack of imp on python2') +# try: +# import yaml +# except ImportError: +# msg.fatal('Cannot find yaml library') +try: + if PY_VERSION == 3: + import pyyaml3 as yaml + else: + import pyyaml2 as yaml +except ImportError: + msg.fatal('Cannot find yaml library') + + +class PythonCharts(object): + """ + Main class used to control every python module. + """ + + def __init__(self, + modules=None, + modules_path='../python.d/', + modules_configs='../conf.d/', + modules_disabled=None): + """ + :param modules: list + :param modules_path: str + :param modules_configs: str + :param modules_disabled: list + """ + + if modules is None: + modules = [] + if modules_disabled is None: + modules_disabled = [] + + self.first_run = True + # set configuration directory + self.configs = modules_configs + + # load modules + loaded_modules = self._load_modules(modules_path, modules, modules_disabled) + + # load configuration files + configured_modules = self._load_configs(loaded_modules) + + # good economy and prosperity: + self.jobs = self._create_jobs(configured_modules) # type: list + + # enable timetable override like `python.d.plugin mysql debug 1` + if DEBUG_FLAG and OVERRIDE_UPDATE_EVERY: + for job in self.jobs: + job.create_timetable(BASE_CONFIG['update_every']) + + @staticmethod + def _import_module(path, name=None): + """ + Try to import module using only its path. + :param path: str + :param name: str + :return: object + """ + + if name is None: + name = path.split('/')[-1] + if name[-len(MODULE_EXTENSION):] != MODULE_EXTENSION: + return None + name = name[:-len(MODULE_EXTENSION)] + try: + if PY_VERSION == 3: + return importlib.machinery.SourceFileLoader(name, path).load_module() + else: + return imp.load_source(name, path) + except Exception as e: + msg.error("Problem loading", name, str(e)) + return None + + def _load_modules(self, path, modules, disabled): + """ + Load modules from 'modules' list or dynamically every file from 'path' (only .chart.py files) + :param path: str + :param modules: list + :param disabled: list + :return: list + """ + + # check if plugin directory exists + if not os.path.isdir(path): + msg.fatal("cannot find charts directory ", path) + + # load modules + loaded = [] + if len(modules) > 0: + for m in modules: + if m in disabled: + continue + mod = self._import_module(path + m + MODULE_EXTENSION) + if mod is not None: + loaded.append(mod) + else: # exit if plugin is not found + msg.fatal('no modules found.') + else: + # scan directory specified in path and load all modules from there + names = os.listdir(path) + for mod in names: + if mod.replace(MODULE_EXTENSION, "") in disabled: + msg.error(mod + ": disabled module ", mod.replace(MODULE_EXTENSION, "")) + continue + m = self._import_module(path + mod) + if m is not None: + msg.debug(mod + ": loading module '" + path + mod + "'") + loaded.append(m) + return loaded + + def _load_configs(self, modules): + """ + Append configuration in list named `config` to every module. + For multi-job modules `config` list is created in _parse_config, + otherwise it is created here based on BASE_CONFIG prototype with None as identifier. + :param modules: list + :return: list + """ + for mod in modules: + configfile = self.configs + mod.__name__ + ".conf" + if os.path.isfile(configfile): + msg.debug(mod.__name__ + ": loading module configuration: '" + configfile + "'") + try: + if not hasattr(mod, 'config'): + mod.config = {} + setattr(mod, + 'config', + self._parse_config(mod, read_config(configfile))) + except Exception as e: + msg.error(mod.__name__ + ": cannot parse configuration file '" + configfile + "':", str(e)) + else: + msg.error(mod.__name__ + ": configuration file '" + configfile + "' not found. Using defaults.") + # set config if not found + if not hasattr(mod, 'config'): + msg.debug(mod.__name__ + ": setting configuration for only one job") + mod.config = {None: {}} + for var in BASE_CONFIG: + try: + mod.config[None][var] = getattr(mod, var) + except AttributeError: + mod.config[None][var] = BASE_CONFIG[var] + return modules + + @staticmethod + def _parse_config(module, config): + """ + Parse configuration file or extract configuration from module file. + Example of returned dictionary: + config = {'name': { + 'update_every': 2, + 'retries': 3, + 'priority': 30000 + 'other_val': 123}} + :param module: object + :param config: dict + :return: dict + """ + if config is None: + config = {} + # get default values + defaults = {} + msg.debug(module.__name__ + ": reading configuration") + for key in BASE_CONFIG: + try: + # get defaults from module config + defaults[key] = int(config.pop(key)) + except (KeyError, ValueError): + try: + # get defaults from module source code + defaults[key] = getattr(module, key) + except (KeyError, ValueError, AttributeError): + # if above failed, get defaults from global dict + defaults[key] = BASE_CONFIG[key] + + # check if there are dict in config dict + many_jobs = False + for name in config: + if type(config[name]) is dict: + many_jobs = True + break + + # assign variables needed by supervisor to every job configuration + if many_jobs: + for name in config: + for key in defaults: + if key not in config[name]: + config[name][key] = defaults[key] + # if only one job is needed, values doesn't have to be in dict (in YAML) + else: + config = {None: config.copy()} + config[None].update(defaults) + + # return dictionary of jobs where every job has BASE_CONFIG variables + return config + + @staticmethod + def _create_jobs(modules): + """ + Create jobs based on module.config dictionary and module.Service class definition. + :param modules: list + :return: list + """ + jobs = [] + for module in modules: + for name in module.config: + # register a new job + conf = module.config[name] + try: + job = module.Service(configuration=conf, name=name) + except Exception as e: + msg.error(module.__name__ + + ("/" + str(name) if name is not None else "") + + ": cannot start job: '" + + str(e)) + return None + else: + # set chart_name (needed to plot run time graphs) + job.chart_name = module.__name__ + if name is not None: + job.chart_name += "_" + name + jobs.append(job) + msg.debug(module.__name__ + ("/" + str(name) if name is not None else "") + ": job added") + + return [j for j in jobs if j is not None] + + def _stop(self, job, reason=None): + """ + Stop specified job and remove it from self.jobs list + Also notifies user about job failure if DEBUG_FLAG is set + :param job: object + :param reason: str + """ + prefix = job.__module__ + if job.name is not None and len(job.name) != 0: + prefix += "/" + job.name + try: + self.jobs.remove(job) + msg.info("Disabled", prefix) + except Exception as e: + msg.debug("This shouldn't happen. NO " + prefix + " IN LIST:" + str(self.jobs) + " ERROR: " + str(e)) + + # TODO remove section below and remove `reason`. + prefix += ": " + if reason is None: + return + elif reason[:3] == "no ": + msg.error(prefix + + "does not seem to have " + + reason[3:] + + "() function. Disabling it.") + elif reason[:7] == "failed ": + msg.error(prefix + + reason[7:] + + "() function reports failure.") + elif reason[:13] == "configuration": + msg.error(prefix + + "configuration file '" + + self.configs + + job.__module__ + + ".conf' not found. Using defaults.") + elif reason[:11] == "misbehaving": + msg.error(prefix + "is " + reason) + + def check(self): + """ + Tries to execute check() on every job. + This cannot fail thus it is catching every exception + If job.check() fails job is stopped + """ + i = 0 + overridden = [] + msg.debug("all job objects", str(self.jobs)) + while i < len(self.jobs): + job = self.jobs[i] + try: + if not job.check(): + msg.error(job.chart_name, "check function failed.") + self._stop(job) + else: + msg.debug(job.chart_name, "check succeeded") + i += 1 + try: + if job.override_name is not None: + new_name = job.__module__ + '_' + job.override_name + if new_name in overridden: + msg.error(job.override_name + " already exists. Stopping '" + job.name + "'") + self._stop(job) + i -= 1 + else: + job.name = job.override_name + msg.debug(job.chart_name + " changing chart name to: '" + new_name + "'") + job.chart_name = new_name + overridden.append(job.chart_name) + except Exception: + pass + except AttributeError as e: + self._stop(job) + msg.error(job.chart_name, "cannot find check() function or it thrown unhandled exception.") + msg.debug(str(e)) + except (UnboundLocalError, Exception) as e: + msg.error(job.chart_name, str(e)) + self._stop(job) + msg.debug("overridden job names:", str(overridden)) + msg.debug("all remaining job objects:", str(self.jobs)) + + def create(self): + """ + Tries to execute create() on every job. + This cannot fail thus it is catching every exception. + If job.create() fails job is stopped. + This is also creating job run time chart. + """ + i = 0 + while i < len(self.jobs): + job = self.jobs[i] + try: + if not job.create(): + msg.error(job.chart_name, "create function failed.") + self._stop(job) + else: + chart = job.chart_name + sys.stdout.write( + "CHART netdata.plugin_pythond_" + + chart + + " '' 'Execution time for " + + chart + + " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " + + str(job.timetable['freq']) + + '\n') + sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n") + msg.debug("created charts for", job.chart_name) + # sys.stdout.flush() + i += 1 + except AttributeError: + msg.error(job.chart_name, "cannot find create() function or it thrown unhandled exception.") + self._stop(job) + except (UnboundLocalError, Exception) as e: + msg.error(job.chart_name, str(e)) + self._stop(job) + + def update(self): + """ + Creates and supervises every job thread. + This will stay forever and ever and ever forever and ever it'll be the one... + """ + for job in self.jobs: + job.start() + + while True: + if threading.active_count() <= 1: + msg.fatal("no more jobs") + time.sleep(1) + + +def read_config(path): + """ + Read YAML configuration from specified file + :param path: str + :return: dict + """ + try: + with open(path, 'r') as stream: + config = yaml.load(stream) + except (OSError, IOError): + msg.error(str(path), "is not a valid configuration file") + return None + except yaml.YAMLError as e: + msg.error(str(path), "is malformed:", e) + return None + return config + + +def parse_cmdline(directory, *commands): + """ + Parse parameters from command line. + :param directory: str + :param commands: list of str + :return: dict + """ + global DEBUG_FLAG + global OVERRIDE_UPDATE_EVERY + global BASE_CONFIG + + changed_update = False + mods = [] + for cmd in commands[1:]: + if cmd == "check": + pass + elif cmd == "debug" or cmd == "all": + DEBUG_FLAG = True + # redirect stderr to stdout? + elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd): + #DEBUG_FLAG = True + mods.append(cmd.replace(".chart.py", "")) + else: + try: + BASE_CONFIG['update_every'] = int(cmd) + changed_update = True + except ValueError: + pass + if changed_update and DEBUG_FLAG: + OVERRIDE_UPDATE_EVERY = True + msg.debug(PROGRAM, "overriding update interval to", str(BASE_CONFIG['update_every'])) + + msg.debug("started from", commands[0], "with options:", *commands[1:]) + + return mods + + +# if __name__ == '__main__': +def run(): + """ + Main program. + """ + global DEBUG_FLAG, BASE_CONFIG + + # read configuration file + disabled = [] + configfile = CONFIG_DIR + "python.d.conf" + msg.PROGRAM = PROGRAM + msg.info("reading configuration file:", configfile) + log_counter = 200 + log_interval = 3600 + + conf = read_config(configfile) + if conf is not None: + try: + # exit the whole plugin when 'enabled: no' is set in 'python.d.conf' + if conf['enabled'] is False: + msg.fatal('disabled in configuration file.\n') + except (KeyError, TypeError): + pass + try: + for param in BASE_CONFIG: + BASE_CONFIG[param] = conf[param] + except (KeyError, TypeError): + pass # use default update_every from NETDATA_UPDATE_EVERY + try: + DEBUG_FLAG = conf['debug'] + except (KeyError, TypeError): + pass + try: + log_counter = conf['logs_per_interval'] + except (KeyError, TypeError): + pass + try: + log_interval = conf['log_interval'] + except (KeyError, TypeError): + pass + for k, v in conf.items(): + if k in ("update_every", "debug", "enabled"): + continue + if v is False: + disabled.append(k) + + # parse passed command line arguments + modules = parse_cmdline(MODULES_DIR, *sys.argv) + msg.DEBUG_FLAG = DEBUG_FLAG + msg.LOG_COUNTER = log_counter + msg.LOG_INTERVAL = log_interval + msg.info("MODULES_DIR='" + MODULES_DIR + + "', CONFIG_DIR='" + CONFIG_DIR + + "', UPDATE_EVERY=" + str(BASE_CONFIG['update_every']) + + ", ONLY_MODULES=" + str(modules)) + + # run plugins + charts = PythonCharts(modules, MODULES_DIR, CONFIG_DIR + "python.d/", disabled) + charts.check() + charts.create() + charts.update() + msg.fatal("finished") + + +if __name__ == '__main__': + run() diff --git a/plugins.d/tc-qos-helper.sh b/plugins.d/tc-qos-helper.sh index 7b4739815..bff5217d2 100755 --- a/plugins.d/tc-qos-helper.sh +++ b/plugins.d/tc-qos-helper.sh @@ -1,107 +1,126 @@ -#!/bin/bash +#!/usr/bin/env bash export PATH="${PATH}:/sbin:/usr/sbin:/usr/local/sbin" +PROGRAM_FILE="$0" +PROGRAM_NAME="$(basename $0)" +PROGRAM_NAME="${PROGRAM_NAME/.plugin}" + +plugins_dir="${NETDATA_PLUGINS_DIR}" +[ -z "$plugins_dir" ] && plugins_dir="$( dirname $PROGRAM_FILE )" + +config_dir=${NETDATA_CONFIG_DIR-/etc/netdata} +tc="$(which tc 2>/dev/null)" +fireqos_run_dir="/var/run/fireqos" +qos_get_class_names_every=120 +qos_exit_every=3600 + +# check if we have a valid number for interval +t=${1} +update_every=$((t)) +[ $((update_every)) -lt 1 ] && update_every=${NETDATA_UPDATE_EVERY} +[ $((update_every)) -lt 1 ] && update_every=1 + +# allow the user to override our defaults +if [ -f "${config_dir}/tc-qos-helper.conf" ] + then + source "${config_dir}/tc-qos-helper.conf" +fi + # default time function now_ms= current_time_ms() { - now_ms="$(date +'%s')000" + now_ms="$(date +'%s')000" } # default sleep function LOOPSLEEPMS_LASTWORK=0 loopsleepms() { - [ "$1" = "tellwork" ] && shift - sleep $1 + [ "$1" = "tellwork" ] && shift + sleep $1 } # if found and included, this file overwrites loopsleepms() # with a high resolution timer function for precise looping. -. "$NETDATA_PLUGINS_DIR/loopsleepms.sh.inc" +. "${plugins_dir}/loopsleepms.sh.inc" -# check if we have a valid number for interval -t=$1 -sleep_time=$((t)) -[ $((sleep_time)) -lt 1 ] && $NETDATA_UPDATE_EVERY -[ $((sleep_time)) -lt 1 ] && sleep_time=1 - -tc_cmd="$(which tc)" -if [ -z "$tc_cmd" ] - then - echo >&2 "tc: Cannot find a 'tc' command in this system." - exit 1 +if [ -z "${tc}" -o ! -x "${tc}" ] + then + echo >&2 "${PROGRAM_NAME}: Cannot find command 'tc' in this system." + exit 1 fi devices= fix_names= setclassname() { - echo "SETCLASSNAME $3 $2" + echo "SETCLASSNAME $3 $2" } show_tc() { - local x="$1" - - echo "BEGIN $x" - $tc_cmd -s class show dev $x - - # check FireQOS names for classes - if [ ! -z "$fix_names" -a -f /var/run/fireqos/ifaces/$x ] - then - name="$(cat /var/run/fireqos/ifaces/$x)" - echo "SETDEVICENAME $name" - - interface_classes= - interface_classes_monitor= - . /var/run/fireqos/$name.conf - for n in $interface_classes_monitor - do - setclassname $(echo $n | tr '|' ' ') - done - echo "SETDEVICEGROUP $interface_dev" - fi - echo "END $x" + local x="${1}" interface_dev interface_classes interface_classes_monitor + + echo "BEGIN ${x}" + ${tc} -s class show dev ${x} + + # check FireQOS names for classes + if [ ! -z "${fix_names}" -a -f "${fireqos_run_dir}/ifaces/${x}" ] + then + name="$(<"${fireqos_run_dir}/ifaces/${x}")" + echo "SETDEVICENAME ${name}" + + interface_dev= + interface_classes= + interface_classes_monitor= + source "${fireqos_run_dir}/${name}.conf" + for n in ${interface_classes_monitor} + do + setclassname ${n//|/ } + done + [ ! -z "${interface_dev}" ] && echo "SETDEVICEGROUP ${interface_dev}" + fi + echo "END ${x}" } all_devices() { - cat /proc/net/dev | grep ":" | cut -d ':' -f 1 | while read dev - do - l=$($tc_cmd class show dev $dev | wc -l) - [ $l -ne 0 ] && echo $dev - done + cat /proc/net/dev | grep ":" | cut -d ':' -f 1 | while read dev + do + l=$(${tc} class show dev ${dev} | wc -l) + [ $l -ne 0 ] && echo ${dev} + done } # update devices and class names # once every 2 minutes -names_every=$((120 / sleep_time)) +names_every=$((qos_get_class_names_every / update_every)) # exit this script every hour # it will be restarted automatically -exit_after=$((3600 / sleep_time)) +exit_after=$((qos_exit_every / update_every)) c=0 gc=0 while [ 1 ] do - fix_names= - c=$((c + 1)) - gc=$((gc + 1)) + fix_names= + c=$((c + 1)) + gc=$((gc + 1)) - if [ $c -le 1 -o $c -ge $names_every ] - then - c=1 - fix_names="YES" - devices="$( all_devices )" - fi + if [ ${c} -le 1 -o ${c} -ge ${names_every} ] + then + c=1 + fix_names="YES" + devices="$( all_devices )" + fi - for d in $devices - do - show_tc $d - done + for d in ${devices} + do + show_tc ${d} + done - echo "WORKTIME $LOOPSLEEPMS_LASTWORK" + echo "WORKTIME ${LOOPSLEEPMS_LASTWORK}" - loopsleepms $sleep_time + loopsleepms ${update_every} - [ $gc -gt $exit_after ] && exit 0 + [ ${gc} -gt ${exit_after} ] && exit 0 done |