# netdata
# real-time performance and health monitoring, done right!
-# (C) 2016 Costa Tsaousis <costa@tsaousis.gr>
+# (C) 2016-2017 Costa Tsaousis <costa@tsaousis.gr>
# GPL v3+
#
# how frequently to collect UPS data
nut_update_every=2
+# how much time in seconds, to wait for nut to respond
nut_timeout=2
+# set this to 1, to enable another chart showing the number
+# of UPS clients connected to upsd
+nut_clients_chart=0
+
# the priority of nut related to other charts
nut_priority=90000
nut_get() {
run -t $nut_timeout upsc "$1"
+
+ if [ "${nut_clients_chart}" -eq "1" ]
+ then
+ printf "ups.connected_clients: "
+ run -t $nut_timeout upsc -c "$1" | wc -l
+ fi
}
nut_check() {
CHART nut_$x.temp '' "UPS Temperature" "temperature" ups nut.temperature line $((nut_priority + 7)) $nut_update_every
DIMENSION temp temp absolute 1 100
EOF
+
+ if [ "${nut_clients_chart}" = "1" ]
+ then
+ cat <<EOF2
+CHART nut_$x.clients '' "UPS Connected Clients" "clients" ups nut.clients area $((nut_priority + 8)) $nut_update_every
+DIMENSION clients '' absolute 1 1
+EOF2
+ fi
+
done
return 0
output_voltage = 0;
load = 0;
temp = 0;
+ client = 0;
+ do_clients = ${nut_clients_chart};
}
/^battery.charge: .*/ { battery_charge = \$2 * 100 };
/^battery.voltage: .*/ { battery_voltage = \$2 * 100 };
/^output.voltage: .*/ { output_voltage = \$2 * 100 };
/^ups.load: .*/ { load = \$2 * 100 };
/^ups.temperature: .*/ { temp = \$2 * 100 };
+/^ups.connected_clients: .*/ { clients = \$2 };
END {
print \"BEGIN nut_$x.charge $1\";
print \"SET battery_charge = \" battery_charge;
print \"BEGIN nut_$x.temp $1\";
print \"SET temp = \" temp;
print \"END\"
+
+ if(do_clients) {
+ print \"BEGIN nut_$x.clients $1\";
+ print \"SET clients = \" clients;
+ print \"END\"
+ }
}"
[ $? -ne 0 ] && unset nut_ids[$i] && error "failed to get values for '$i', disabling it."
done
python.d/exim.conf \
python.d/fail2ban.conf \
python.d/freeradius.conf \
- python.d/gunicorn_log.conf \
python.d/haproxy.conf \
python.d/hddtemp.conf \
python.d/ipfs.conf \
python.d/memcached.conf \
python.d/mysql.conf \
python.d/nginx.conf \
- python.d/nginx_log.conf \
python.d/ovpn_status_log.conf \
python.d/phpfpm.conf \
python.d/postfix.conf \
python.d/smartd_log.conf \
python.d/tomcat.conf \
python.d/varnish.conf \
+ python.d/web_log.conf \
$(NULL)
healthconfigdir=$(configdir)/health.d
health.d/retroshare.conf \
health.d/squid.conf \
health.d/varnish.conf \
+ health.d/web_log.conf \
$(NULL)
if LINUX
# netdata
# real-time performance and health monitoring, done right!
-# (C) 2016 Costa Tsaousis <costa@tsaousis.gr>
+# (C) 2016-2017 Costa Tsaousis <costa@tsaousis.gr>
# GPL v3+
# a space separated list of UPS names
# how much time in seconds, to wait for nut to respond
#nut_timeout=2
+# set this to 1, to enable another chart showing the number
+# of UPS clients connected to upsd
+#nut_clients_chart=1
+
# the data collection frequency
# if unset, will inherit the netdata update frequency
#nut_update_every=2
--- /dev/null
+
+# make sure we can collect web log data
+
+template: last_collected_secs
+ on: web_log.response_codes
+families: *
+ calc: $now - $last_collected_t
+ units: seconds ago
+ every: 10s
+ warn: $this > (($status >= $WARNING) ? ($update_every) : ( 5 * $update_every))
+ crit: $this > (($status == $CRITICAL) ? ($update_every) : (60 * $update_every))
+ delay: down 5m multiplier 1.5 max 1h
+ info: number of seconds since the last successful data collection
+ to: webmaster
+
+
+# -----------------------------------------------------------------------------
+# high level response code alarms
+
+# the following alarms trigger only when there are enough data.
+# we assume there are enough data when:
+#
+# $1m_requests > 120
+#
+# i.e. when there are at least 120 requests during the last minute
+
+template: 1m_requests
+ on: web_log.response_codes
+families: *
+ lookup: sum -1m unaligned
+ calc: ($this == 0)?(1):($this)
+ units: requests
+ every: 10s
+ info: the sum of all HTTP requests over the last minute
+
+template: 1m_2xx
+ on: web_log.response_codes
+families: *
+ lookup: sum -1m unaligned of 2xx
+ calc: ($this == 0)?(1):($this)
+ units: requests
+ every: 10s
+ info: the sum of successful HTTP requests over the last minute
+
+template: 1m_successful
+ on: web_log.response_codes
+families: *
+ calc: $1m_2xx * 100 / $1m_requests
+ units: %
+ every: 10s
+ warn: ($1m_requests > 120) ? ($this < (($status >= $WARNING ) ? ( 98 ) : ( 95 )) ) : ( 0 )
+ crit: ($1m_requests > 120) ? ($this < (($status == $CRITICAL) ? ( 95 ) : ( 90 )) ) : ( 0 )
+ delay: down 15m multiplier 1.5 max 1h
+ info: the ratio of successful HTTP responses (2xx) over the last minute
+ to: webmaster
+
+template: 1m_redirects
+ on: web_log.detailed_response_codes
+families: *
+ lookup: sum -1m unaligned of 301,303,307,308
+ calc: $this * 100 / $1m_requests
+ units: %
+ every: 10s
+ warn: ($1m_requests > 120) ? ($this > (($status >= $WARNING ) ? ( 1 ) : ( 2 )) ) : ( 0 )
+ crit: ($1m_requests > 120) ? ($this > (($status == $CRITICAL) ? ( 2 ) : ( 5 )) ) : ( 0 )
+ delay: down 15m multiplier 1.5 max 1h
+ info: the ratio of HTTP redirects (301, 303, 307, 308) over the last minute
+ to: webmaster
+
+template: 1m_bad_requests
+ on: web_log.response_codes
+families: *
+ lookup: sum -1m unaligned of 4xx
+ calc: $this * 100 / $1m_requests
+ units: %
+ every: 10s
+ warn: ($1m_requests > 120) ? ($this > (($status >= $WARNING) ? ( 1 ) : ( 5 )) ) : ( 0 )
+ crit: ($1m_requests > 120) ? ($this > (($status == $CRITICAL) ? ( 5 ) : ( 10 )) ) : ( 0 )
+ delay: down 15m multiplier 1.5 max 1h
+ info: the ratio of HTTP bad requests (4xx) over the last minute
+ to: webmaster
+
+template: 1m_internal_errors
+ on: web_log.response_codes
+families: *
+ lookup: sum -1m unaligned of 5xx
+ calc: $this * 100 / $1m_requests
+ units: %
+ every: 10s
+ warn: ($1m_requests > 120) ? ($this > (($status >= $WARNING) ? ( 1 ) : ( 2 )) ) : ( 0 )
+ crit: ($1m_requests > 120) ? ($this > (($status == $CRITICAL) ? ( 2 ) : ( 5 )) ) : ( 0 )
+ delay: down 15m multiplier 1.5 max 1h
+ info: the ratio of HTTP internal server errors (5xx), over the last minute
+ to: webmaster
+
+
+# -----------------------------------------------------------------------------
+# web slow
+
+# the following alarms trigger only when there are enough data.
+# we assume there are enough data when:
+#
+# $1m_requests > 120
+#
+# i.e. when there are at least 120 requests during the last minute
+
+template: 10m_response_time
+ on: web_log.response_time
+families: *
+ lookup: average -10m unaligned of avg
+ units: ms
+ every: 30s
+ info: the average time to respond to HTTP requests, over the last 10 minutes
+
+template: web_slow
+ on: web_log.response_time
+families: *
+ lookup: average -1m unaligned of avg
+ units: ms
+ every: 10s
+ green: 500
+ red: 1000
+ warn: ($1m_requests > 120) ? ($this > $green && $this > ($10m_response_time * 2) ) : ( 0 )
+ crit: ($1m_requests > 120) ? ($this > $red && $this > ($10m_response_time * 4) ) : ( 0 )
+ delay: down 15m multiplier 1.5 max 1h
+ info: the average time to respond to HTTP requests, over the last 1 minute
+ to: webmaster
+
+# -----------------------------------------------------------------------------
+# web too many or too few requests
+
+# the following alarms trigger only when there are enough data.
+# we assume there are enough data when:
+#
+# $5m_2xx_last > 120
+#
+# i.e. when there were at least 120 requests during the 5 minutes starting
+# at -10m and ending at -5m
+
+template: 5m_2xx_last
+ on: web_log.response_codes
+families: *
+ lookup: average -5m at -5m unaligned of 2xx
+ units: requests
+ every: 30s
+ info: average successful HTTP requests over the last 5 minutes
+
+template: 5m_2xx_now
+ on: web_log.response_codes
+families: *
+ lookup: average -5m unaligned of 2xx
+ units: requests
+ every: 30s
+ info: average successful HTTP requests over the last 5 minutes
+
+template: 5m_requests_ratio
+ on: web_log.response_codes
+families: *
+ calc: ($5m_2xx_last > 0)?($5m_2xx_now * 100 / $5m_2xx_last):(100)
+ units: %
+ every: 30s
+ warn: ($5m_2xx_last > 120) ? ($this > 200 OR $this < 50) : (0)
+ crit: ($5m_2xx_last > 120) ? ($this > 400 OR $this < 25) : (0)
+ delay: down 15m multiplier 1.5 max 1h
+options: no-clear-notification
+ info: the percentage of web requests over the last 5 minutes, \
+ compared with the previous 5 minutes
+ to: webmaster
+
# The default for all modules is enabled (yes).
# Setting any of these to no will disable it.
-# apache: yes
# apache_cache: yes
+# apache: yes
+# bind_rndc: yes
# cpufreq: yes
+# cpuidle: yes
# dovecot: yes
+# elasticsearch: yes
+
+# this is just an example
example: no
+
# exim: yes
+# fail2ban: yes
+# freeradius: yes
+
+# gunicorn_log has been replaced by web_log
+gunicorn_log: no
+
+# haproxy: yes
# hddtemp: yes
# ipfs: yes
# isc_dhcpd: yes
+# mdstat: yes
# memcached: yes
# mysql: yes
# nginx: yes
-# nginx_log: yes
+
+# nginx_log has been replaced by web_log
+nginx_log: no
+
+# ovpn_status_log: yes
# phpfpm: yes
# postfix: yes
+# postgres: yes
# redis: yes
+# retroshare: yes
# sensors: yes
+# smartd_log: yes
# squid: yes
# tomcat: yes
-# freeradius: yes
-# ovpn_status_log: yes
+# varnish: yes
+# web_log: yes
--- /dev/null
+# netdata python.d.plugin configuration for web log
+#
+# This file is in YaML format. Generally the format is:
+#
+# name: value
+#
+# There are 2 sections:
+# - global variables
+# - one or more JOBS
+#
+# JOBS allow you to collect values from multiple sources.
+# Each source will have its own set of charts.
+#
+# JOB parameters have to be indented (using spaces only, example below).
+
+# ----------------------------------------------------------------------
+# Global Variables
+# These variables set the defaults for all JOBs, however each JOB
+# may define its own, overriding the defaults.
+
+# update_every sets the default data collection frequency.
+# If unset, the python.d.plugin default is used.
+# update_every: 1
+
+# priority controls the order of charts at the netdata dashboard.
+# Lower numbers move the charts towards the top of the page.
+# If unset, the default for python.d.plugin is used.
+# priority: 60000
+
+# retries sets the number of retries to be made in case of failures.
+# If unset, the default for python.d.plugin is used.
+# Attempts to restore the service are made once every update_every
+# and only if the module has collected values in the past.
+# retries: 5
+
+# ----------------------------------------------------------------------
+# JOBS (data collection sources)
+#
+# The default JOBS share the same *name*. JOBS with the same name
+# are mutually exclusive. Only one of them will be allowed running at
+# any time. This allows autodetection to try several alternatives and
+# pick the one that works.
+#
+# Any number of jobs is supported.
+
+# ----------------------------------------------------------------------
+# PLUGIN CONFIGURATION
+#
+# All python.d.plugin JOBS (for all its modules) support a set of
+# predefined parameters. These are:
+#
+# job_name:
+# name: myname # the JOB's name as it will appear at the
+# # dashboard (by default is the job_name)
+# # JOBs sharing a name are mutually exclusive
+# update_every: 1 # the JOB's data collection frequency
+# priority: 60000 # the JOB's order on the dashboard
+# retries: 5 # the JOB's number of restoration attempts
+#
+# Additionally to the above, web_log also supports the following:
+#
+# path: 'PATH' # the path to web server log file
+# detailed_response_codes: yes/no # Default: yes. Additional chart where response codes are not grouped
+# all_time : yes/no # Default: yes. All time unique client IPs chart (50000 addresses ~ 400KB)
+# categories: # requests per url chart configuration
+# cacti: 'cacti.*' # name(dimension): REGEX to match
+# observium: 'observium.*' # name(dimension): REGEX to match
+# stub_status: 'stub_status' # name(dimension): REGEX to match
+
+# ----------------------------------------------------------------------
+# WEB SERVER CONFIGURATION
+#
+# Make sure the log directory and file can be read by user 'netdata'.
+#
+# Preferable Log Format. You need to change to this to collect all metrics.
+#
+# nginx:
+# log_format netdata '$remote_addr - $remote_user [$time_local] '
+# '"$request" $status $body_bytes_sent '
+# '$request_length $request_time '
+# '"$http_referer" "$http_user_agent"';
+# access_log /var/log/nginx/access.log netdata;
+#
+# apache:
+# LogFormat "%h %l %u %t \"%r\" %>s %O %I %D \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
+# LogFormat "%h %l %u %t \"%r\" %>s %O %I %D \"%{Referer}i\" \"%{User-Agent}i\"" combined
+
+# ----------------------------------------------------------------------
+# AUTO-DETECTION JOBS
+# only one of them per web server will run (when they have the same name)
+
+
+# -------------------------------------------
+# nginx log on various distros
+
+# debian, arch
+nginx_log:
+ name: 'nginx'
+ path: '/var/log/nginx/access.log'
+
+# gentoo
+nginx_log2:
+ name: 'nginx'
+ path: '/var/log/nginx/localhost.access_log'
+
+
+# -------------------------------------------
+# apache log on various distros
+
+# debian
+apache_log:
+ name: 'apache'
+ path: '/var/log/apache2/access.log'
+
+# gentoo
+apache_log2:
+ name: 'apache'
+ path: '/var/log/apache2/access_log'
+
+# arch
+apache_log3:
+ name: 'apache'
+ path: '/var/log/httpd/access_log'
+
+# debian
+apache_vhosts_log:
+ name: 'apache_vhosts'
+ path: '/var/log/apache2/other_vhosts_access.log'
+
+
+# -------------------------------------------
+# gunicorn log on various distros
+
+gunicorn_log:
+ name: 'gunicorn'
+ path: '/var/log/gunicorn/access.log'
+
+gunicorn_log2:
+ name: 'gunicorn'
+ path: '/var/log/gunicorn/gunicorn-access.log'
declare -A configs_signatures=(
['0056936ce99788ed9ae1c611c87aa6d8']='apps_groups.conf'
['0102351817595a85d01ebd54a5f2f36b']='python.d/ovpn_status_log.conf'
+ ['01302e01162d465614276de43fad7546']='python.d.conf'
['02fa10fa85ab88e9723998de48d1aca0']='health.d/disks.conf'
['036dc300bd7b0e0ef229b9822686d63e']='python.d/isc_dhcpd.conf'
['0388b873d0d7e47c19005b7241db77d8']='python.d/tomcat.conf'
['043f0a35dde85837fabeb85b990a41c1']='health.d/swap.conf'
['0529b679d3c0e7e6332753c7f6484731']='health.d/net.conf'
['057d12aaff0467e64529e839a258806b']='health.d/entropy.conf'
+ ['059d98d0c562e1c81653d1e64673deab']='python.d/web_log.conf'
+ ['05a8f39f134850c1e8d6267dbe706273']='health.d/web_log.conf'
['061c45b0e34170d357e47883166ecf40']='python.d/nginx.conf'
['074df527cc70b5f38c0714f08f20e57c']='health.d/apache.conf'
['08042325ab27256b938575deafee8ecf']='python.d/nginx.conf'
['5e6fd588ef6934cf04ddb5e662aa02ea']='health.d/postgres.conf'
['5eb670b6fe39da5fec2523d910b0dd1e']='health.d/cpu.conf'
['5f05d4b248ab2637ada319b4e8c4e4c3']='python.d/varnish.conf'
+ ['5f109df927d5f20409c81f4bfca0c83e']='python.d/web_log.conf'
['5ff1bcaa58695754e2f6980bfe19f579']='health.d/entropy.conf'
['61b7ed36f35e7bd930f5f7f91694a112']='charts.d/postfix.conf'
['621f10b257a11add5ff5aff41e9662e3']='health.d/memcached.conf'
['623771eecb3c277fc728b5304793f93b']='health.d/cpu.conf'
+ ['632c28d714c87a4969d11cf36a5edaa8']='health.d/web_log.conf'
['636d032928ea0f4741eab264fb49c099']='apps_groups.conf'
['6398ef37a15cb6a0bc921f58948d2b39']='health.d/softnet.conf'
['64070d856ab1b47a18ec871e49bbc13b']='python.d/squid.conf'
['64c48f9726ab987baec9c617a9fef7a6']='health.d/nginx.conf'
['64ffc1b6878c81b87564b0f48642c790']='health.d/elasticsearch.conf'
['650b5fc9da23b25ee7ee1481e4aa2851']='health_alarm_notify.conf'
+ ['653e0c014c8fcfb4db6cd3351d87d720']='python.d.conf'
['6546909d10cc5efcef9dd873bea85956']='python.d/mysql.conf'
['65c6933a17fb6b7f8e6baeab73431c17']='charts.d/apcupsd.conf'
['6608c6546b3c6bde084fc1d34b1163c1']='health.d/retroshare.conf'
['6cba40e32a7e98a98c31a209913839cc']='python.d/nginx_log.conf'
['6d02c2dd0863e09ad9dbba53e3b58116']='health.d/mysql.conf'
['6ea958ca521e0514af57c08b518d8c5c']='health.d/backend.conf'
+ ['6f303ccfdc21c7b122758cea8c15e249']='python.d.conf'
['70105b1744a8e13f49083d7f1981aea2']='python.d/ipfs.conf'
['707a63f53f4b32e01d134ae90ba94aad']='health_alarm_notify.conf'
['707a63f53f4b32e01d134ae90ba94aad']='health_email_recipients.conf'
['70d82dabecb09a1da4684f293abef0c9']='health_alarm_notify.conf'
+ ['729b3e24a72f7d566fd429617d51a21b']='health.d/web_log.conf'
['73125ae64d5c6e9361944cd9bd14844e']='python.d/exim.conf'
['731a1fcfe9b2da1b9d685056a59541b8']='python.d/hddtemp.conf'
['73a8e10dfe4183aca751e9e2a80dabe3']='node.d.conf'
['99b6030ce25c8fee4598179c0f95fb0b']='health.d/redis.conf'
['99c1617448abbdc493976ab9bda5ce02']='apps_groups.conf'
['9a8a459a3841b78d4c6ef07428ad2fe1']='health.d/entropy.conf'
+ ['9b6eee7f2febb29efac2b7ea9fcab9be']='charts.d/nut.conf'
['9c0185ceff15415bc59b2ce2c1f04367']='apps_groups.conf'
['9c8ddfa810d83ae58c8614ee5229e66b']='health.d/disks.conf'
['9c981c75bdf4b1637f7113e7e45eb2bf']='health.d/memcached.conf'
['df7e8044902b5e155fad8430c2ddcfa8']='health.d/fping.conf'
['dfd5431b11cf2f3852a40d390c1d5a92']='python.d/varnish.conf'
['e0242003fd2e3f9ac1b9314e802ada79']='python.d/hddtemp.conf'
+ ['e0ba3bc216ffc9933b4741dbb6b1f8c8']='health.d/web_log.conf'
['e0e96cc47ed61d6492416be5236cd4d3']='python.d/apache_cache.conf'
['e2f3388c06726154c10ec22bad5bc7ec']='fping.conf'
['e3023092e3b2bbb5351e0fe6682f4fe9']='health_alarm_notify.conf'
['e734c5951a8764d4d9de046dd7cf7407']='health.d/softnet.conf'
['e7bc22a1942cffbd2b1b0cfd119ee328']='health.d/ipfs.conf'
['e8ec8046c7007af6ca3e8c51e62c99f8']='health.d/disks.conf'
+ ['eaa7beb935cae9c48a40fb934eb105a7']='health.d/web_log.conf'
['eb5168f0b516bc982aac45e59da6e52e']='health.d/nginx.conf'
['eb748d6fb69d11b0d29c5794657e206c']='health.d/qos.conf'
['ebd0612ccc5807524ebb2b647e3e56c9']='apps_groups.conf'
['ee5343881744e6a97e6ee5cdd329cfb8']='health.d/retroshare.conf'
['ef1861bf5725d91e773cbdba05687597']='python.d.conf'
['ef9916ea144878a9f37cbb6b1b29da10']='health.d/squid.conf'
+ ['f1446cb3f1a905ee06defa2aa15ee806']='python.d/web_log.conf'
['f2f1b8656f5011e965ac45b818cf668d']='apps_groups.conf'
['f42df9f13abfae2426519c6728b34882']='charts.d/example.conf'
['f4c5d88c34d3fb853498124177cc77f1']='python.d.conf'
['f5736e0b2945182cb659cb0713eff923']='apps_groups.conf'
['f66e5236ba1245bb2e5fd99191f114c6']='charts.d/hddtemp.conf'
['f6c6656f900ff52d159dca12d624016a']='python.d/postgres.conf'
+ ['f7401a6e7c7d4fe2e0e2be7f7f523275']='health.d/web_log.conf'
['f7a99e94231beda85c6254912d8d31c1']='python.d/tomcat.conf'
['f82924563e41d99cdae5431f0af69155']='python.d.conf'
['f8c30f22df92765e2c0fab3c8174e2fc']='health.d/memcached.conf'
NETDATA_ADDED_TO_NGINX=0
NETDATA_ADDED_TO_VARNISH=0
NETDATA_ADDED_TO_HAPROXY=0
+NETDATA_ADDED_TO_ADM=0
if [ ${UID} -eq 0 ]
then
portable_add_group netdata
portable_add_user_to_group nginx netdata && NETDATA_ADDED_TO_NGINX=1
portable_add_user_to_group varnish netdata && NETDATA_ADDED_TO_VARNISH=1
portable_add_user_to_group haproxy netdata && NETDATA_ADDED_TO_HAPROXY=1
+ portable_add_user_to_group adm netdata && NETDATA_ADDED_TO_ADM=1
if [ -d /etc/logrotate.d -a ! -f /etc/logrotate.d/netdata ]
then
echo " gpasswd -d netdata haproxy"
fi
+getent group adm > /dev/null
+if [ $? -eq 0 -a "${NETDATA_ADDED_TO_ADM}" = "1" ]
+ then
+ echo
+ echo "You may also want to remove the netdata user from the adm group"
+ echo "by running:"
+ echo " gpasswd -d netdata adm"
+fi
+
UNINSTALL
chmod 750 netdata-uninstaller.sh
global DEBUG_FLAG, TRACE_FLAG, BASE_CONFIG
# read configuration file
- disabled = []
+ disabled = ['nginx_log', 'gunicorn_log']
configfile = CONFIG_DIR + "python.d.conf"
msg.PROGRAM = PROGRAM
msg.info("reading configuration file:", configfile)
exim.chart.py \
fail2ban.chart.py \
freeradius.chart.py \
- gunicorn_log.chart.py \
haproxy.chart.py \
hddtemp.chart.py \
ipfs.chart.py \
memcached.chart.py \
mysql.chart.py \
nginx.chart.py \
- nginx_log.chart.py \
ovpn_status_log.chart.py \
phpfpm.chart.py \
postfix.chart.py \
smartd_log.chart.py \
tomcat.chart.py \
varnish.chart.py \
+ web_log.chart.py \
python-modules-installer.sh \
$(NULL)
+++ /dev/null
-# -*- coding: utf-8 -*-
-# Description: nginx log netdata python.d module
-# Author: Pawel Krupa (paulfantom)
-# Modified for Gunicorn by: Jeff Willette (deltaskelta)
-
-from base import LogService
-import re
-
-priority = 60000
-retries = 60
-# update_every = 3
-
-ORDER = ['codes']
-CHARTS = {
- 'codes': {
- 'options': [None, 'gunicorn status codes', 'requests/s', 'requests', 'gunicorn_log.codes', 'stacked'],
- 'lines': [
- ["2xx", None, "incremental"],
- ["3xx", None, "incremental"],
- ["4xx", None, "incremental"],
- ["5xx", None, "incremental"]
- ]}
-}
-
-
-class Service(LogService):
- def __init__(self, configuration=None, name=None):
- LogService.__init__(self, configuration=configuration, name=name)
- if len(self.log_path) == 0:
- self.log_path = "/var/log/gunicorn/access.log"
- self.order = ORDER
- self.definitions = CHARTS
- pattern = r'" ([0-9]{3}) '
- #pattern = r'(?:" )([0-9][0-9][0-9]) ?'
- self.regex = re.compile(pattern)
-
- def _get_data(self):
- """
- Parse new log lines
- :return: dict
- """
- data = {'2xx': 0,
- '3xx': 0,
- '4xx': 0,
- '5xx': 0}
- try:
- raw = self._get_raw_data()
- if raw is None:
- return None
- elif not raw:
- return data
- except (ValueError, AttributeError):
- return None
-
- regex = self.regex
- for line in raw:
- code = regex.search(line)
- try:
- beginning = code.group(1)[0]
- except AttributeError:
- continue
-
- if beginning == '2':
- data["2xx"] += 1
- elif beginning == '3':
- data["3xx"] += 1
- elif beginning == '4':
- data["4xx"] += 1
- elif beginning == '5':
- data["5xx"] += 1
-
- return data
+++ /dev/null
-# -*- coding: utf-8 -*-
-# Description: nginx log netdata python.d module
-# Author: Pawel Krupa (paulfantom)
-
-from base import LogService
-import re
-
-priority = 60000
-retries = 60
-# update_every = 3
-
-ORDER = ['codes']
-CHARTS = {
- 'codes': {
- 'options': [None, 'nginx status codes', 'requests/s', 'requests', 'nginx_log.codes', 'stacked'],
- 'lines': [
- ["2xx", None, "incremental"],
- ["5xx", None, "incremental"],
- ["3xx", None, "incremental"],
- ["4xx", None, "incremental"],
- ["1xx", None, "incremental"],
- ["other", None, "incremental"]
- ]}
-}
-
-
-class Service(LogService):
- def __init__(self, configuration=None, name=None):
- LogService.__init__(self, configuration=configuration, name=name)
- if len(self.log_path) == 0:
- self.log_path = "/var/log/nginx/access.log"
- self.order = ORDER
- self.definitions = CHARTS
- pattern = r'" ([0-9]{3}) ?'
- #pattern = r'(?:" )([0-9][0-9][0-9]) ?'
- self.regex = re.compile(pattern)
-
- self.data = {
- '1xx': 0,
- '2xx': 0,
- '3xx': 0,
- '4xx': 0,
- '5xx': 0,
- 'other': 0
- }
-
- def _get_data(self):
- """
- Parse new log lines
- :return: dict
- """
- try:
- raw = self._get_raw_data()
- if raw is None:
- return None
- elif not raw:
- return self.data
- except (ValueError, AttributeError):
- return None
-
- regex = self.regex
- for line in raw:
- code = regex.search(line)
- try:
- beginning = code.group(1)[0]
- except AttributeError:
- continue
-
- if beginning == '2':
- self.data["2xx"] += 1
- elif beginning == '3':
- self.data["3xx"] += 1
- elif beginning == '4':
- self.data["4xx"] += 1
- elif beginning == '5':
- self.data["5xx"] += 1
- elif beginning == '1':
- self.data["1xx"] += 1
- else:
- self.data["other"] += 1
-
- return self.data
--- /dev/null
+# -*- coding: utf-8 -*-
+# Description: web log netdata python.d module
+# Author: l2isbad
+
+from base import LogService
+import re
+import bisect
+from os import access, R_OK
+from os.path import getsize
+from collections import namedtuple
+from copy import deepcopy
+try:
+ from itertools import zip_longest
+except ImportError:
+ from itertools import izip_longest as zip_longest
+
+priority = 60000
+retries = 60
+
+ORDER = ['response_codes', 'bandwidth', 'response_time', 'requests_per_url', 'http_method', 'requests_per_ipproto',
+ 'clients', 'clients_all']
+CHARTS = {
+ 'response_codes': {
+ 'options': [None, 'Response Codes', 'requests/s', 'responses', 'web_log.response_codes', 'stacked'],
+ 'lines': [
+ ['2xx', '2xx', 'incremental'],
+ ['5xx', '5xx', 'incremental'],
+ ['3xx', '3xx', 'incremental'],
+ ['4xx', '4xx', 'incremental'],
+ ['1xx', '1xx', 'incremental'],
+ ['0xx', 'other', 'incremental'],
+ ['unmatched', 'unmatched', 'incremental']
+ ]},
+ 'bandwidth': {
+ 'options': [None, 'Bandwidth', 'KB/s', 'bandwidth', 'web_log.bandwidth', 'area'],
+ 'lines': [
+ ['resp_length', 'received', 'incremental', 1, 1024],
+ ['bytes_sent', 'sent', 'incremental', -1, 1024]
+ ]},
+ 'response_time': {
+ 'options': [None, 'Processing Time', 'milliseconds', 'timings', 'web_log.response_time', 'area'],
+ 'lines': [
+ ['resp_time_min', 'min', 'incremental', 1, 1000],
+ ['resp_time_max', 'max', 'incremental', 1, 1000],
+ ['resp_time_avg', 'avg', 'incremental', 1, 1000]
+ ]},
+ 'clients': {
+ 'options': [None, 'Current Poll Unique Client IPs', 'unique ips', 'clients', 'web_log.clients', 'stacked'],
+ 'lines': [
+ ['unique_cur_ipv4', 'ipv4', 'incremental', 1, 1],
+ ['unique_cur_ipv6', 'ipv6', 'incremental', 1, 1]
+ ]},
+ 'clients_all': {
+ 'options': [None, 'All Time Unique Client IPs', 'unique ips', 'clients', 'web_log.clients_all', 'stacked'],
+ 'lines': [
+ ['unique_tot_ipv4', 'ipv4', 'absolute', 1, 1],
+ ['unique_tot_ipv6', 'ipv6', 'absolute', 1, 1]
+ ]},
+ 'http_method': {
+ 'options': [None, 'Requests Per HTTP Method', 'requests/s', 'http methods', 'web_log.http_method', 'stacked'],
+ 'lines': [
+ ]},
+ 'requests_per_ipproto': {
+ 'options': [None, 'Requests Per IP Protocol', 'requests/s', 'ip protocols', 'web_log.requests_per_ipproto',
+ 'stacked'],
+ 'lines': [
+ ['req_ipv4', 'ipv4', 'incremental', 1, 1],
+ ['req_ipv6', 'ipv6', 'incremental', 1, 1]
+ ]}
+}
+
+NAMED_URL_PATTERN = namedtuple('URL_PATTERN', ['description', 'pattern'])
+
+
+class Service(LogService):
+ def __init__(self, configuration=None, name=None):
+ LogService.__init__(self, configuration=configuration, name=name)
+ # Variables from module configuration file
+ self.log_path = self.configuration.get('path')
+ self.detailed_response_codes = self.configuration.get('detailed_response_codes', True)
+ self.all_time = self.configuration.get('all_time', True)
+ self.url_pattern = self.configuration.get('categories') # dict
+ self.regex = None # will be assigned in 'find_regex' method
+ self.resp_time_func = None # will be assigned in 'find_regex' method
+ self._get_data = None # will be assigned in 'check' method.
+ self.order = None # will be assigned in 'create_*_method' method.
+ self.definitions = None # will be assigned in 'create_*_method' method.
+ self.detailed_chart = None # will be assigned in 'create_*_method' method.
+ self.http_method_chart = None # will be assigned in 'create_*_method' method.
+ # sorted list of unique IPs
+ self.unique_all_time = list()
+ # if there is no new logs this dict returned to netdata
+ self.data = {'bytes_sent': 0, 'resp_length': 0, 'resp_time_min': 0,
+ 'resp_time_max': 0, 'resp_time_avg': 0, 'unique_cur_ipv4': 0,
+ 'unique_cur_ipv6': 0, '2xx': 0, '5xx': 0, '3xx': 0, '4xx': 0,
+ '1xx': 0, '0xx': 0, 'unmatched': 0, 'req_ipv4': 0, 'req_ipv6': 0,
+ 'unique_tot_ipv4': 0, 'unique_tot_ipv6': 0}
+
+ def check(self):
+ if not self.log_path:
+ self.error('log path is not specified')
+ return False
+
+ # log_path must be readable
+ if not access(self.log_path, R_OK):
+ self.error('%s not readable or not exist' % self.log_path)
+ return False
+
+ # log_path file should not be empty
+ if not getsize(self.log_path):
+ self.error('%s is empty' % self.log_path)
+ return False
+
+ # Read last line (or first if there is only one line)
+ with open(self.log_path, 'rb') as logs:
+ logs.seek(-2, 2)
+ while logs.read(1) != b'\n':
+ logs.seek(-2, 1)
+ if logs.tell() == 0:
+ break
+ last_line = logs.readline().decode(encoding='utf-8')
+
+ # Parse last line
+ regex_name = self.find_regex(last_line)
+ if not regex_name:
+ self.error('Can\'t parse %s' % self.log_path)
+ return False
+
+ if regex_name.startswith('access_'):
+ self.create_access_charts(regex_name)
+ if regex_name == 'access_default':
+ self.info('Not all data collected. You need to modify LogFormat.')
+ self._get_data = self._get_access_data
+ self.info('Used regex: %s' % regex_name)
+ return True
+ else:
+ # If it's not access_logs.. Not used at the moment
+ return False
+
+ def find_regex(self, last_line):
+ """
+ :param last_line: str: literally last line from log file
+ :return: regex_name
+ It's sad but different web servers has different logs formats
+ We need to find appropriate regex for current log file
+ All logic is do a regex search through the string for all patterns
+ until we find something or fail.
+ """
+ # REGEX: 1.IPv4 address 2.HTTP method 3. URL 4. Response code
+ # 5. Bytes sent 6. Response length 7. Response process time
+ access_default = re.compile(r'([\da-f.:]+)'
+ r' -.*?"([A-Z]+)'
+ r' (.*?)"'
+ r' ([1-9]\d{2})'
+ r' (\d+)')
+
+ access_apache_ext = re.compile(r'([\da-f.:]+)'
+ r' -.*?"([A-Z]+)'
+ r' (.*?)"'
+ r' ([1-9]\d{2})'
+ r' (\d+)'
+ r' (\d+)'
+ r' (\d+) ')
+
+ access_nginx_ext = re.compile(r'([\da-f.:]+)'
+ r' -.*?"([A-Z]+)'
+ r' (.*?)"'
+ r' ([1-9]\d{2})'
+ r' (\d+)'
+ r' (\d+)'
+ r' ([\d.]+) ')
+
+ regex_function = zip([access_apache_ext, access_nginx_ext, access_default],
+ [lambda x: x, lambda x: x * 1000000, lambda x: x],
+ ['access_apache_ext', 'access_nginx_ext', 'access_default'])
+ regex_name = None
+ for regex, function, name in regex_function:
+ if regex.search(last_line):
+ self.regex = regex
+ self.resp_time_func = function
+ regex_name = name
+ break
+ return regex_name
+
+ def create_access_charts(self, regex_name):
+ """
+ :param regex_name: str: regex name from 'find_regex' method. Ex.: 'apache_extended', 'nginx_extended'
+ :return:
+ Create additional charts depending on the 'find_regex' result (parsed_line) and configuration file
+ 1. 'time_response' chart is removed if there is no 'time_response' in logs.
+ 2. Other stuff is just remove/add chart depending on yes/no in conf
+ """
+ def find_job_name(override_name, name):
+ """
+ :param override_name: str: 'name' var from configuration file
+ :param name: str: 'job_name' from configuration file
+ :return: str: new job name
+ We need this for dynamic charts. Actually same logic as in python.d.plugin.
+ """
+ add_to_name = override_name or name
+ if add_to_name:
+ return '_'.join(['web_log', re.sub('\s+', '_', add_to_name)])
+ else:
+ return 'web_log'
+
+ self.order = ORDER[:]
+ self.definitions = deepcopy(CHARTS)
+
+ job_name = find_job_name(self.override_name, self.name)
+ self.detailed_chart = 'CHART %s.detailed_response_codes ""' \
+ ' "Detailed Response Codes" requests/s responses' \
+ ' web_log.detailed_response_codes stacked 1 %s\n' % (job_name, self.update_every)
+ self.http_method_chart = 'CHART %s.http_method' \
+ ' "" "Requests Per HTTP Method" requests/s "http methods"' \
+ ' web_log.http_method stacked 2 %s\n' % (job_name, self.update_every)
+
+ # Remove 'request_time' chart from ORDER if request_time not in logs
+ if regex_name == 'access_default':
+ self.order.remove('response_time')
+ # Remove 'clients_all' chart from ORDER if specified in the configuration
+ if not self.all_time:
+ self.order.remove('clients_all')
+ # Add 'detailed_response_codes' chart if specified in the configuration
+ if self.detailed_response_codes:
+ self.order.append('detailed_response_codes')
+ self.definitions['detailed_response_codes'] = {'options': [None, 'Detailed Response Codes', 'requests/s',
+ 'responses', 'web_log.detailed_response_codes',
+ 'stacked'],
+ 'lines': []}
+
+ # Add 'requests_per_url' chart if specified in the configuration
+ if self.url_pattern:
+ self.url_pattern = [NAMED_URL_PATTERN(description=k, pattern=re.compile(v)) for k, v
+ in self.url_pattern.items()]
+ self.definitions['requests_per_url'] = {'options': [None, 'Requests Per Url', 'requests/s',
+ 'urls', 'web_log.requests_per_url', 'stacked'],
+ 'lines': [['other_url', 'other', 'incremental']]}
+ for elem in self.url_pattern:
+ self.definitions['requests_per_url']['lines'].append([elem.description, elem.description,
+ 'incremental'])
+ self.data.update({elem.description: 0})
+ self.data.update({'other_url': 0})
+ else:
+ self.order.remove('requests_per_url')
+
+ def add_new_dimension(self, dimension, line_list, chart_string, key):
+ """
+ :param dimension: str: response status code. Ex.: '202', '499'
+ :param line_list: list: Ex.: ['202', '202', 'incremental']
+ :param chart_string: Current string we need to pass to netdata to rebuild the chart
+ :param key: str: CHARTS dict key (chart name). Ex.: 'response_time'
+ :return: str: new chart string = previous + new dimensions
+ """
+ self.data.update({dimension: 0})
+ # SET method check if dim in _dimensions
+ self._dimensions.append(dimension)
+ # UPDATE method do SET only if dim in definitions
+ self.definitions[key]['lines'].append(line_list)
+ chart = chart_string
+ chart += "%s %s\n" % ('DIMENSION', ' '.join(line_list))
+ print(chart)
+ return chart
+
+ def _get_access_data(self):
+ """
+ Parse new log lines
+ :return: dict OR None
+ None if _get_raw_data method fails.
+ In all other cases - dict.
+ """
+ raw = self._get_raw_data()
+ if raw is None:
+ return None
+
+ request_time, unique_current = list(), list()
+ request_counter = {'count': 0, 'sum': 0}
+ ip_address_counter = {'unique_cur_ip': 0}
+ for line in raw:
+ match = self.regex.search(line)
+ if match:
+ match_dict = dict(zip_longest('address method url code sent resp_length resp_time'.split(),
+ match.groups()))
+ try:
+ code = ''.join([match_dict['code'][0], 'xx'])
+ self.data[code] += 1
+ except KeyError:
+ self.data['0xx'] += 1
+ # detailed response code
+ if self.detailed_response_codes:
+ self._get_data_detailed_response_codes(match_dict['code'])
+ # requests per url
+ if self.url_pattern:
+ self._get_data_per_url(match_dict['url'])
+ # requests per http method
+ self._get_data_http_method(match_dict['method'])
+ # bandwidth sent
+ self.data['bytes_sent'] += int(match_dict['sent'])
+ # request processing time and bandwidth received
+ if match_dict['resp_length'] and match_dict['resp_time']:
+ self.data['resp_length'] += int(match_dict['resp_length'])
+ resp_time = self.resp_time_func(float(match_dict['resp_time']))
+ bisect.insort_left(request_time, resp_time)
+ request_counter['count'] += 1
+ request_counter['sum'] += resp_time
+ # requests per ip proto
+ proto = 'ipv4' if '.' in match_dict['address'] else 'ipv6'
+ self.data['req_' + proto] += 1
+ # unique clients ips
+ if address_not_in_pool(self.unique_all_time, match_dict['address'],
+ self.data['unique_tot_ipv4'] + self.data['unique_tot_ipv6']):
+ self.data['unique_tot_' + proto] += 1
+ if address_not_in_pool(unique_current, match_dict['address'], ip_address_counter['unique_cur_ip']):
+ self.data['unique_cur_' + proto] += 1
+ ip_address_counter['unique_cur_ip'] += 1
+ else:
+ self.data['unmatched'] += 1
+ # timings
+ if request_time:
+ self.data['resp_time_min'] += int(request_time[0])
+ self.data['resp_time_avg'] += int(round(float(request_counter['sum']) / request_counter['count']))
+ self.data['resp_time_max'] += int(request_time[-1])
+ return self.data
+
+ def _get_data_detailed_response_codes(self, code):
+ """
+ :param code: str: CODE from parsed line. Ex.: '202, '499'
+ :return:
+ Calls add_new_dimension method If the value is found for the first time
+ """
+ if code not in self.data:
+ chart_string_copy = self.detailed_chart
+ self.detailed_chart = self.add_new_dimension(code, [code, code, 'incremental'],
+ chart_string_copy, 'detailed_response_codes')
+ self.data[code] += 1
+
+ def _get_data_http_method(self, method):
+ """
+ :param method: str: METHOD from parsed line. Ex.: 'GET', 'POST'
+ :return:
+ Calls add_new_dimension method If the value is found for the first time
+ """
+ if method not in self.data:
+ chart_string_copy = self.http_method_chart
+ self.http_method_chart = self.add_new_dimension(method, [method, method, 'incremental'],
+ chart_string_copy, 'http_method')
+ self.data[method] += 1
+
+ def _get_data_per_url(self, url):
+ """
+ :param url: str: URL from parsed line
+ :return:
+ Scan through string looking for the first location where patterns produce a match for all user
+ defined patterns
+ """
+ match = None
+ for elem in self.url_pattern:
+ if elem.pattern.search(url):
+ self.data[elem.description] += 1
+ match = True
+ break
+ if not match:
+ self.data['other_url'] += 1
+
+
+def address_not_in_pool(pool, address, pool_size):
+ """
+ :param pool: list of ip addresses
+ :param address: ip address
+ :param pool_size: current size of pool
+ :return: True if address not in pool. False if address in pool
+ """
+ index = bisect.bisect_left(pool, address)
+ if index < pool_size:
+ if pool[index] == address:
+ return False
+ else:
+ bisect.insort_left(pool, address)
+ return True
+ else:
+ bisect.insort_left(pool, address)
+ return True
return 1;
}
-static inline char *tabs2spaces(char *s) {
- char *t = s;
- while(*t) {
- if(unlikely(*t == '\t')) *t = ' ';
- t++;
+static inline char *trim_all_spaces(char *buffer) {
+ char *d = buffer, *s = buffer;
+
+ // skip spaces
+ while(isspace(*s)) s++;
+
+ while(*s) {
+ // copy the non-space part
+ while(*s && !isspace(*s)) *d++ = *s++;
+
+ // add a space if we have to
+ if(*s && isspace(*s)) {
+ *d++ = ' ';
+ s++;
+ }
+
+ // skip spaces
+ while(isspace(*s)) s++;
+ }
+
+ *d = '\0';
+
+ if(d > buffer) {
+ d--;
+ if(isspace(*d)) *d = '\0';
}
- return s;
+ if(!buffer[0]) return NULL;
+ return buffer;
}
static inline char *health_source_file(size_t line, const char *path, const char *filename) {
s++;
char *value = s;
- key = trim(key);
- value = trim(value);
+ key = trim_all_spaces(key);
+ value = trim_all_spaces(value);
if(!key) {
error("Health configuration has invalid line %zu of file '%s/%s'. Keyword is empty. Ignoring it.", line, path, filename);
rc = callocz(1, sizeof(RRDCALC));
rc->next_event_id = 1;
- rc->name = tabs2spaces(strdupz(value));
+ rc->name = strdupz(value);
rc->hash = simple_hash(rc->name);
rc->source = health_source_file(line, path, filename);
rc->green = NAN;
rrdcalctemplate_free(&localhost, rt);
rt = callocz(1, sizeof(RRDCALCTEMPLATE));
- rt->name = tabs2spaces(strdupz(value));
+ rt->name = strdupz(value);
rt->hash_name = simple_hash(rt->name);
rt->source = health_source_file(line, path, filename);
rt->green = NAN;
freez(rc->chart);
}
- rc->chart = tabs2spaces(strdupz(value));
+ rc->chart = strdupz(value);
rc->hash_chart = simple_hash(rc->chart);
}
else if(hash == hash_lookup && !strcasecmp(key, HEALTH_LOOKUP_KEY)) {
freez(rc->exec);
}
- rc->exec = tabs2spaces(strdupz(value));
+ rc->exec = strdupz(value);
}
else if(hash == hash_recipient && !strcasecmp(key, HEALTH_RECIPIENT_KEY)) {
if(rc->recipient) {
freez(rc->recipient);
}
- rc->recipient = tabs2spaces(strdupz(value));
+ rc->recipient = strdupz(value);
}
else if(hash == hash_units && !strcasecmp(key, HEALTH_UNITS_KEY)) {
if(rc->units) {
freez(rc->units);
}
- rc->units = tabs2spaces(strdupz(value));
+ rc->units = strdupz(value);
strip_quotes(rc->units);
}
else if(hash == hash_info && !strcasecmp(key, HEALTH_INFO_KEY)) {
freez(rc->info);
}
- rc->info = tabs2spaces(strdupz(value));
+ rc->info = strdupz(value);
strip_quotes(rc->info);
}
else if(hash == hash_delay && !strcasecmp(key, HEALTH_DELAY_KEY)) {
freez(rt->context);
}
- rt->context = tabs2spaces(strdupz(value));
+ rt->context = strdupz(value);
rt->hash_context = simple_hash(rt->context);
}
else if(hash == hash_families && !strcasecmp(key, HEALTH_FAMILIES_KEY)) {
freez(rt->family_match);
simple_pattern_free(rt->family_pattern);
- rt->family_match = tabs2spaces(strdupz(value));
+ rt->family_match = strdupz(value);
rt->family_pattern = simple_pattern_create(rt->family_match, SIMPLE_PATTERN_EXACT);
}
else if(hash == hash_lookup && !strcasecmp(key, HEALTH_LOOKUP_KEY)) {
freez(rt->exec);
}
- rt->exec = tabs2spaces(strdupz(value));
+ rt->exec = strdupz(value);
}
else if(hash == hash_recipient && !strcasecmp(key, HEALTH_RECIPIENT_KEY)) {
if(rt->recipient) {
freez(rt->recipient);
}
- rt->recipient = tabs2spaces(strdupz(value));
+ rt->recipient = strdupz(value);
}
else if(hash == hash_units && !strcasecmp(key, HEALTH_UNITS_KEY)) {
if(rt->units) {
freez(rt->units);
}
- rt->units = tabs2spaces(strdupz(value));
+ rt->units = strdupz(value);
strip_quotes(rt->units);
}
else if(hash == hash_info && !strcasecmp(key, HEALTH_INFO_KEY)) {
freez(rt->info);
}
- rt->info = tabs2spaces(strdupz(value));
+ rt->info = strdupz(value);
strip_quotes(rt->info);
}
else if(hash == hash_delay && !strcasecmp(key, HEALTH_DELAY_KEY)) {
int rrddim_algorithm_id(const char *name)
{
- if(strcmp(name, RRDDIM_INCREMENTAL_NAME) == 0) return RRDDIM_INCREMENTAL;
- if(strcmp(name, RRDDIM_ABSOLUTE_NAME) == 0) return RRDDIM_ABSOLUTE;
- if(strcmp(name, RRDDIM_PCENT_OVER_ROW_TOTAL_NAME) == 0) return RRDDIM_PCENT_OVER_ROW_TOTAL;
+ if(strcmp(name, RRDDIM_INCREMENTAL_NAME) == 0) return RRDDIM_INCREMENTAL;
+ if(strcmp(name, RRDDIM_ABSOLUTE_NAME) == 0) return RRDDIM_ABSOLUTE;
+ if(strcmp(name, RRDDIM_PCENT_OVER_ROW_TOTAL_NAME) == 0) return RRDDIM_PCENT_OVER_ROW_TOTAL;
if(strcmp(name, RRDDIM_PCENT_OVER_DIFF_TOTAL_NAME) == 0) return RRDDIM_PCENT_OVER_DIFF_TOTAL;
return RRDDIM_ABSOLUTE;
}
error("File %s does not have the same divisor. Clearing it.", fullfilename);
memset(rd, 0, size);
}
- else if(rd->algorithm != algorithm) {
- errno = 0;
- error("File %s does not have the same algorithm. Clearing it.", fullfilename);
- memset(rd, 0, size);
- }
else if(rd->update_every != st->update_every) {
errno = 0;
error("File %s does not have the same refresh frequency. Clearing it.", fullfilename);
// rd = NULL;
memset(rd, 0, size);
}
+
+ if(rd->algorithm && rd->algorithm != algorithm)
+ error("File %s does not have the expected algorithm (expected %d '%s', found %d '%s'). Previous values may be wrong.", fullfilename, algorithm, rrddim_algorithm_name(algorithm), rd->algorithm, rrddim_algorithm_name(rd->algorithm));
}
if(rd) {
lib/d3-3.5.17.min.js \
lib/dygraph-combined-dd74404.js \
lib/dygraph-smooth-plotter-dd74404.js \
- lib/gauge-1.3.1.min.js \
+ lib/gauge-1.3.2.min.js \
lib/jquery-2.2.4.min.js \
lib/jquery.easypiechart-97b5824.min.js \
lib/perfect-scrollbar-0.6.15.min.js \
NETDATA.peity_js = NETDATA.serverDefault + 'lib/jquery.peity-3.2.0.min.js';
NETDATA.sparkline_js = NETDATA.serverDefault + 'lib/jquery.sparkline-2.1.2.min.js';
NETDATA.easypiechart_js = NETDATA.serverDefault + 'lib/jquery.easypiechart-97b5824.min.js';
- NETDATA.gauge_js = NETDATA.serverDefault + 'lib/gauge-1.3.1.min.js';
+ NETDATA.gauge_js = NETDATA.serverDefault + 'lib/gauge-1.3.2.min.js';
NETDATA.dygraph_js = NETDATA.serverDefault + 'lib/dygraph-combined-dd74404.js';
NETDATA.dygraph_smooth_js = NETDATA.serverDefault + 'lib/dygraph-smooth-plotter-dd74404.js';
NETDATA.raphael_js = NETDATA.serverDefault + 'lib/raphael-2.2.4-min.js';
var netdataDashboard = window.netdataDashboard || {};
-// menu
+// ----------------------------------------------------------------------------
+// menus
+
// information about the main menus
netdataDashboard.menu = {
'tc': {
title: 'Quality of Service',
icon: '<i class="fa fa-globe" aria-hidden="true"></i>',
- info: 'Netdata collects and visualizes tc class utilization using its <a href="https://github.com/firehol/netdata/blob/master/plugins.d/tc-qos-helper.sh" target="_blank">tc-helper plugin</a>. If you also use <a href="http://firehol.org/#fireqos" target="_blank">FireQOS</a> for setting up QoS, netdata automatically collects interface and class names. If your QoS configuration includes overheads calculation, the values shown here will include these overheads (the total bandwidth for the same interface as reported in the Network Interfaces section, will be lower than the total bandwidth reported here). QoS data collection may have a slight time difference compared to the interface (QoS data collection uses a BASH script, so a shift in data collection of a few milliseconds should be justified).'
+ info: 'Netdata collects and visualizes <code>tc</code> class utilization using its <a href="https://github.com/firehol/netdata/blob/master/plugins.d/tc-qos-helper.sh" target="_blank">tc-helper plugin</a>. If you also use <a href="http://firehol.org/#fireqos" target="_blank">FireQOS</a> for setting up QoS, netdata automatically collects interface and class names. If your QoS configuration includes overheads calculation, the values shown here will include these overheads (the total bandwidth for the same interface as reported in the Network Interfaces section, will be lower than the total bandwidth reported here). QoS data collection may have a slight time difference compared to the interface (QoS data collection uses a BASH script, so a shift in data collection of a few milliseconds should be justified).'
},
'net': {
info: undefined
},
+ 'web_log': {
+ title: undefined,
+ icon: '<i class="fa fa-file-text-o" aria-hidden="true"></i>',
+ info: 'Information extracted from a web server log file. <code>python.d/web_log</code> plugin incrementally parses the web server log file to provide, in real-time, a break down of key web server performance metrics. A special log file format may optionally be used (for <code>nginx</code> and <code>apache</code>) allowing the plugin to extract timing information for the web server responses and bandwidth for both requests and responses. <code>web_log</code> plugin may also be configured to provide a break down of requests per URL pattern (check <a href="https://github.com/firehol/netdata/blob/master/conf.d/python.d/web_log.conf" target="_blank"><code>/etc/netdata/python.d/web_log.conf</code></a>). netdata attaches several alarms on these charts, such as <b>too many bad requests</b>, <b>too many redirects</b>, <b>too many internal errors</b>, <b>unreasonably slow responses</b>, <b>unreasonably many requests</b> (i.e. web attack) and <b>unreasonably few requests</b> (i.e. something is wrong).'
+ },
+
'named': {
title: 'named',
icon: '<i class="fa fa-tag" aria-hidden="true"></i>',
}
};
-// submenu
+
+
+// ----------------------------------------------------------------------------
+// submenus
+
+// information to be shown, just below each submenu
+
// information about the submenus
netdataDashboard.submenu = {
+ 'web_log.bandwidth': {
+ info: 'Bandwidth of requests (<code>received</code>) and responses (<code>sent</code>). <code>received</code> requires a special file format (without it, the web server log does not have this information). This chart may present unusual spikes, since the whole bandwidth will be accounted at the time the log line is saved by the web server, even if the time needed to serve it spans across a longer duration. We suggest to use QoS (e.g. <a href="http://firehol.org/#fireqos" target="_blank">FireQOS</a>) for accurate accounting of the web server bandwidth.'
+ },
+
+ 'web_log.urls': {
+ info: 'Number of requests for each URL <code>category</code> (URL pattern) defined in <a href="https://github.com/firehol/netdata/blob/master/conf.d/python.d/web_log.conf" target="_blank"><code>/etc/netdata/python.d/web_log.conf</code></a>. This chart counts all requests matching the URL patterns defined, independently of the web server response codes (i.e. both successful and unsuccessful).'
+ },
+
+ 'web_log.clients': {
+ info: 'Charts showing the number of unique client IPs, accessing the web server.'
+ },
+
+ 'web_log.timings': {
+ info: 'Web server response timings - the time the web server needed to prepare and respond to requests. This requires a special log format and its meaning is web server specific. For most web servers this accounts the time from the reception of a complete request, to the dispatch of the last byte of the response. So, it includes the network delays of responses, but it does not include the network delays of requests.'
+ },
+
'mem.ksm': {
title: 'Memory Deduper',
info: 'Kernel Same-page Merging (KSM) performance monitoring, read from several files in <code>/sys/kernel/mm/ksm/</code>. KSM is a memory-saving de-duplication feature in the Linux kernel (since version 2.6.32). The KSM daemon ksmd periodically scans those areas of user memory which have been registered with it, looking for pages of identical content which can be replaced by a single write-protected page (which is automatically copied if a process later wants to update its content). KSM was originally developed for use with KVM (where it was known as Kernel Shared Memory), to fit more virtual machines into physical memory, by sharing the data common between them. But it can be useful to any application which generates many instances of the same data.'
}
};
+
+
+// ----------------------------------------------------------------------------
// chart
+
// information works on the context of a chart
// Its purpose is to set:
//
'fping.packets': {
height: 0.5
+ },
+
+ 'web_log.response_codes': {
+ info: 'Break down of web server responses by response code type. <code>1xx</code> are informational responses, <code>2xx</code> are successful responses, <code>3xx</code> are redirects, <code>4xx</code> are bad requests, <code>5xx</code> are internal server errors, <code>other</code> are non-standard responses, <code>unmatched</code> counts the lines in the log file that are not matched by the plugin (please <a href="https://github.com/firehol/netdata/issues/new?title=web_log%20reports%20unmatched%20lines&body=web_log%20plugin%20reports%20unmatched%20lines.%0A%0AThis%20is%20my%20log:%0A%0A%60%60%60txt%0A%0Aplease%20paste%20your%20web%20server%20log%20here%0A%0A%60%60%60" target="_blank">open a github issue</a> to help us fix it, if you have any unmatched lines).',
+
+ mainheads: [
+ function(os, id) {
+ void(os);
+ return '<div data-netdata="' + id + '"'
+ + ' data-dimensions="2xx"'
+ + ' data-chart-library="gauge"'
+ + ' data-title="Successful"'
+ + ' data-units="requests/s"'
+ + ' data-gauge-adjust="width"'
+ + ' data-width="12%"'
+ + ' data-before="0"'
+ + ' data-after="-CHART_DURATION"'
+ + ' data-points="CHART_DURATION"'
+ + ' data-common-max="' + id + '"'
+ + ' data-colors="' + NETDATA.colors[0] + '"'
+ + ' data-decimal-digits="0"'
+ + ' role="application"></div>';
+ },
+
+ function(os, id) {
+ void(os);
+ return '<div data-netdata="' + id + '"'
+ + ' data-dimensions="3xx"'
+ + ' data-chart-library="gauge"'
+ + ' data-title="Redirects"'
+ + ' data-units="requests/s"'
+ + ' data-gauge-adjust="width"'
+ + ' data-width="12%"'
+ + ' data-before="0"'
+ + ' data-after="-CHART_DURATION"'
+ + ' data-points="CHART_DURATION"'
+ + ' data-common-max="' + id + '"'
+ + ' data-colors="' + NETDATA.colors[2] + '"'
+ + ' data-decimal-digits="0"'
+ + ' role="application"></div>';
+ },
+
+ function(os, id) {
+ void(os);
+ return '<div data-netdata="' + id + '"'
+ + ' data-dimensions="4xx"'
+ + ' data-chart-library="gauge"'
+ + ' data-title="Bad Requests"'
+ + ' data-units="requests/s"'
+ + ' data-gauge-adjust="width"'
+ + ' data-width="12%"'
+ + ' data-before="0"'
+ + ' data-after="-CHART_DURATION"'
+ + ' data-points="CHART_DURATION"'
+ + ' data-common-max="' + id + '"'
+ + ' data-colors="' + NETDATA.colors[3] + '"'
+ + ' data-decimal-digits="0"'
+ + ' role="application"></div>';
+ },
+
+ function(os, id) {
+ void(os);
+ return '<div data-netdata="' + id + '"'
+ + ' data-dimensions="5xx"'
+ + ' data-chart-library="gauge"'
+ + ' data-title="Server Errors"'
+ + ' data-units="requests/s"'
+ + ' data-gauge-adjust="width"'
+ + ' data-width="12%"'
+ + ' data-before="0"'
+ + ' data-after="-CHART_DURATION"'
+ + ' data-points="CHART_DURATION"'
+ + ' data-common-max="' + id + '"'
+ + ' data-colors="' + NETDATA.colors[1] + '"'
+ + ' data-decimal-digits="0"'
+ + ' role="application"></div>';
+ }
+ ]
+ },
+
+ 'web_log.response_time': {
+ mainheads: [
+ function(os, id) {
+ void(os);
+ return '<div data-netdata="' + id + '"'
+ + ' data-dimensions="avg"'
+ + ' data-chart-library="gauge"'
+ + ' data-title="Average Response Time"'
+ + ' data-units="milliseconds"'
+ + ' data-gauge-adjust="width"'
+ + ' data-width="12%"'
+ + ' data-before="0"'
+ + ' data-after="-CHART_DURATION"'
+ + ' data-points="CHART_DURATION"'
+ + ' data-colors="' + NETDATA.colors[4] + '"'
+ + ' data-decimal-digits="2"'
+ + ' role="application"></div>';
+ }
+ ]
+ },
+
+ 'web_log.detailed_response_codes': {
+ info: 'Number of responses for each response code.'
+ },
+
+ 'web_log.requests_per_ipproto': {
+ info: 'Web server requests received per IP protocol version.'
+ },
+
+ 'web_log.clients': {
+ info: 'Unique client IPs accessing the web server, within each data collection iteration. If data collection is <b>per second</b>, this chart shows <b>unique client IPs per second</b>.'
+ },
+
+ 'web_log.clients_all': {
+ info: 'Unique client IPs accessing the web server since the last restart of netdata. This plugin keeps in memory all the unique IPs that have accessed the web server. On very busy web servers (several millions of unique IPs) you may want to disable this chart (check <a href="https://github.com/firehol/netdata/blob/master/conf.d/python.d/web_log.conf" target="_blank"><code>/etc/netdata/python.d/web_log.conf</code></a>).'
}
};
submenuTitle: function(menu, submenu) {
var key = menu + '.' + submenu;
+ // console.log(key);
var title = this.anyAttribute(this.submenu, 'title', key, submenu).toString().replace(/_/g, ' ');
if(title.length > 28) {
var a = title.substring(0, 13);
// enrich the data structure returned by netdata
// to reflect our menu system and content
+ // FIXME: this is a shame - we should fix charts naming (issue #807)
function enrichChartData(chart) {
- var tmp = chart.type.split('_')[0];
+ var parts = chart.type.split('_');
+ var tmp = parts[0];
switch(tmp) {
case 'ap':
chart.menu = tmp;
break;
+ case 'apache':
+ chart.menu = chart.type;
+ if(parts.length > 2 && parts[1] === 'cache')
+ chart.menu_pattern = tmp + '_' + parts[1];
+ break;
+
+ case 'bind':
+ chart.menu = chart.type;
+ if(parts.length > 2 && parts[1] === 'rndc')
+ chart.menu_pattern = tmp + '_' + parts[1];
+ break;
+
case 'cgroup':
chart.menu = chart.type;
if(chart.id.match(/.*[\._\/-:]qemu[\._\/-:]*/) || chart.id.match(/.*[\._\/-:]kvm[\._\/-:]*/))
chart.menu_pattern = 'cgroup';
break;
- case 'apache':
- case 'exim':
+ case 'isc':
+ chart.menu = chart.type;
+ if(parts.length > 2 && parts[1] === 'dhcpd')
+ chart.menu_pattern = tmp + '_' + parts[1];
+ break;
+
+ case 'ovpn':
+ chart.menu = chart.type;
+ if(parts.length > 3 && parts[1] === 'status' && parts[2] === 'log')
+ chart.menu_pattern = tmp + '_' + parts[1];
+ break;
+
+ case 'smartd':
+ case 'web':
+ chart.menu = chart.type;
+ if(parts.length > 2 && parts[1] === 'log')
+ chart.menu_pattern = tmp + '_' + parts[1];
+ break;
+
case 'dovecot':
+ case 'exim':
case 'hddtemp':
case 'ipfs':
case 'memcached':
function renderChartsAndMenu(data) {
var menus = options.menus;
var charts = data.charts;
+ var m, menu_key;
for(var c in charts) {
if(!charts.hasOwnProperty(c)) continue;
- enrichChartData(charts[c]);
+ var chart = charts[c];
+ enrichChartData(chart);
+ m = chart.menu;
// create the menu
- if(typeof menus[charts[c].menu] === 'undefined') {
- menus[charts[c].menu] = {
- priority: charts[c].priority,
+ if(typeof menus[m] === 'undefined') {
+ menus[m] = {
+ menu_pattern: chart.menu_pattern,
+ priority: chart.priority,
submenus: {},
- title: netdataDashboard.menuTitle(charts[c]),
- icon: netdataDashboard.menuIcon(charts[c]),
- info: netdataDashboard.menuInfo(charts[c]),
- height: netdataDashboard.menuHeight(charts[c]) * options.chartsHeight
+ title: netdataDashboard.menuTitle(chart),
+ icon: netdataDashboard.menuIcon(chart),
+ info: netdataDashboard.menuInfo(chart),
+ height: netdataDashboard.menuHeight(chart) * options.chartsHeight
};
}
+ else {
+ if(typeof(menus[m].menu_pattern) === 'undefined')
+ menus[m].menu_pattern = chart.menu_pattern;
+
+ if(chart.priority < menus[m].priority)
+ menus[m].priority = chart.priority;
+ }
- if(charts[c].priority < menus[charts[c].menu].priority)
- menus[charts[c].menu].priority = charts[c].priority;
+ menu_key = (typeof(menus[m].menu_pattern) !== 'undefined')?menus[m].menu_pattern:m;
// create the submenu
- if(typeof menus[charts[c].menu].submenus[charts[c].submenu] === 'undefined') {
- menus[charts[c].menu].submenus[charts[c].submenu] = {
- priority: charts[c].priority,
+ if(typeof menus[m].submenus[chart.submenu] === 'undefined') {
+ menus[m].submenus[chart.submenu] = {
+ priority: chart.priority,
charts: [],
title: null,
- info: netdataDashboard.submenuInfo(charts[c].menu, charts[c].submenu),
- height: netdataDashboard.submenuHeight(charts[c].menu, charts[c].submenu, menus[charts[c].menu].height)
+ info: netdataDashboard.submenuInfo(menu_key, chart.submenu),
+ height: netdataDashboard.submenuHeight(menu_key, chart.submenu, menus[m].height)
};
}
-
- if(charts[c].priority < menus[charts[c].menu].submenus[charts[c].submenu].priority)
- menus[charts[c].menu].submenus[charts[c].submenu].priority = charts[c].priority;
+ else {
+ if (chart.priority < menus[m].submenus[chart.submenu].priority)
+ menus[m].submenus[chart.submenu].priority = chart.priority;
+ }
// index the chart in the menu/submenu
- menus[charts[c].menu].submenus[charts[c].submenu].charts.push(charts[c]);
+ menus[m].submenus[chart.submenu].charts.push(chart);
}
// propagate the descriptive subname given to QoS
// to all the other submenus with the same name
- for(var m in menus) {
+ for(m in menus) {
if(!menus.hasOwnProperty(m)) continue;
for(var s in menus[m].submenus) {
menus[m].submenus[s].title = s + ' (' + options.submenu_names[s] + ')';
}
else {
- menus[m].submenus[s].title = netdataDashboard.submenuTitle(m, s);
+ menu_key = (typeof(menus[m].menu_pattern) !== 'undefined')?menus[m].menu_pattern:m;
+ menus[m].submenus[s].title = netdataDashboard.submenuTitle(menu_key, s);
}
}
}
});
NETDATA.requiredJs.push({
- url: NETDATA.serverDefault + 'dashboard_info.js?v20170208-8',
+ url: NETDATA.serverDefault + 'dashboard_info.js?v20170211-20',
async: false,
isAlreadyLoaded: function() { return false; }
});
</div>
</body>
</html>
-<script type="text/javascript" src="dashboard.js?v20170208-8"></script>
+<script type="text/javascript" src="dashboard.js?v20170211-1"></script>
+++ /dev/null
-(function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p=[].slice,q={}.hasOwnProperty,r=function(a,b){function c(){this.constructor=a}for(var d in b)q.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a};!function(){var a,b,c,d,e,f,g;for(g=["ms","moz","webkit","o"],c=0,e=g.length;e>c&&(f=g[c],!window.requestAnimationFrame);c++)window.requestAnimationFrame=window[f+"RequestAnimationFrame"],window.cancelAnimationFrame=window[f+"CancelAnimationFrame"]||window[f+"CancelRequestAnimationFrame"];return a=null,d=0,b={},requestAnimationFrame?window.cancelAnimationFrame?void 0:(a=window.requestAnimationFrame,window.requestAnimationFrame=function(c,e){var f;return f=++d,a(function(){return b[f]?void 0:c()},e),f},window.cancelAnimationFrame=function(a){return b[a]=!0}):(window.requestAnimationFrame=function(a,b){var c,d,e,f;return c=(new Date).getTime(),f=Math.max(0,16-(c-e)),d=window.setTimeout(function(){return a(c+f)},f),e=c+f,d},window.cancelAnimationFrame=function(a){return clearTimeout(a)})}(),String.prototype.hashCode=function(){var a,b,c,d,e;if(b=0,0===this.length)return b;for(c=d=0,e=this.length;e>=0?e>d:d>e;c=e>=0?++d:--d)a=this.charCodeAt(c),b=(b<<5)-b+a,b&=b;return b},o=function(a){var b,c;for(b=Math.floor(a/3600),c=Math.floor((a-3600*b)/60),a-=3600*b+60*c,a+="",c+="";c.length<2;)c="0"+c;for(;a.length<2;)a="0"+a;return b=b?b+":":"",b+c+":"+a},m=function(){var a,b,c;return b=1<=arguments.length?p.call(arguments,0):[],c=b[0],a=b[1],k(c.toFixed(a))},n=function(a,b){var c,d,e;d={};for(c in a)q.call(a,c)&&(e=a[c],d[c]=e);for(c in b)q.call(b,c)&&(e=b[c],d[c]=e);return d},k=function(a){var b,c,d,e;for(a+="",c=a.split("."),d=c[0],e="",c.length>1&&(e="."+c[1]),b=/(\d+)(\d{3})/;b.test(d);)d=d.replace(b,"$1,$2");return d+e},l=function(a){return"#"===a.charAt(0)?a.substring(1,7):a},j=function(){function a(a,b){null==a&&(a=!0),this.clear=null!=b?b:!0,a&&AnimationUpdater.add(this)}return a.prototype.animationSpeed=32,a.prototype.update=function(a){var b;return null==a&&(a=!1),a||this.displayedValue!==this.value?(this.ctx&&this.clear&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),b=this.value-this.displayedValue,Math.abs(b/this.animationSpeed)<=.001?this.displayedValue=this.value:this.displayedValue=this.displayedValue+b/this.animationSpeed,this.render(),!0):!1},a}(),e=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return r(b,a),b.prototype.displayScale=1,b.prototype.setTextField=function(a,b){return this.textField=a instanceof i?a:new i(a,b)},b.prototype.setMinValue=function(a,b){var c,d,e,f,g;if(this.minValue=a,null==b&&(b=!0),b){for(this.displayedValue=this.minValue,f=this.gp||[],g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push(c.displayedValue=this.minValue);return g}},b.prototype.setOptions=function(a){return null==a&&(a=null),this.options=n(this.options,a),this.textField&&(this.textField.el.style.fontSize=a.fontSize+"px"),this.options.angle>.5&&(this.options.angle=.5),this.configDisplayScale(),this},b.prototype.configDisplayScale=function(){var a,b,c,d,e;return d=this.displayScale,this.options.highDpiSupport===!1?delete this.displayScale:(b=window.devicePixelRatio||1,a=this.ctx.webkitBackingStorePixelRatio||this.ctx.mozBackingStorePixelRatio||this.ctx.msBackingStorePixelRatio||this.ctx.oBackingStorePixelRatio||this.ctx.backingStorePixelRatio||1,this.displayScale=b/a),this.displayScale!==d&&(e=this.canvas.G__width||this.canvas.width,c=this.canvas.G__height||this.canvas.height,this.canvas.width=e*this.displayScale,this.canvas.height=c*this.displayScale,this.canvas.style.width=e+"px",this.canvas.style.height=c+"px",this.canvas.G__width=e,this.canvas.G__height=c),this},b}(j),i=function(){function a(a,b){this.el=a,this.fractionDigits=b}return a.prototype.render=function(a){return this.el.innerHTML=m(a.displayedValue,this.fractionDigits)},a}(),a=function(a){function b(a,b){this.elem=a,this.text=null!=b?b:!1,this.value=1*this.elem.innerHTML,this.text&&(this.value=0)}return r(b,a),b.prototype.displayedValue=0,b.prototype.value=0,b.prototype.setVal=function(a){return this.value=1*a},b.prototype.render=function(){var a;return a=this.text?o(this.displayedValue.toFixed(0)):k(m(this.displayedValue)),this.elem.innerHTML=a},b}(j),b={create:function(b){var c,d,e,f;for(f=[],d=0,e=b.length;e>d;d++)c=b[d],f.push(new a(c));return f}},h=function(a){function b(a){this.gauge=a,this.ctx=this.gauge.ctx,this.canvas=this.gauge.canvas,b.__super__.constructor.call(this,!1,!1),this.setOptions()}return r(b,a),b.prototype.displayedValue=0,b.prototype.value=0,b.prototype.options={strokeWidth:.035,length:.1,color:"#000000"},b.prototype.setOptions=function(a){return null==a&&(a=null),this.options=n(this.options,a),this.length=2*this.gauge.radius*this.gauge.options.radiusScale*this.options.length,this.strokeWidth=this.canvas.height*this.options.strokeWidth,this.maxValue=this.gauge.maxValue,this.minValue=this.gauge.minValue,this.animationSpeed=this.gauge.animationSpeed,this.options.angle=this.gauge.options.angle},b.prototype.render=function(){var a,b,c,d,e,f,g;return a=this.gauge.getAngle.call(this,this.displayedValue),f=Math.round(this.length*Math.cos(a)),g=Math.round(this.length*Math.sin(a)),d=Math.round(this.strokeWidth*Math.cos(a-Math.PI/2)),e=Math.round(this.strokeWidth*Math.sin(a-Math.PI/2)),b=Math.round(this.strokeWidth*Math.cos(a+Math.PI/2)),c=Math.round(this.strokeWidth*Math.sin(a+Math.PI/2)),this.ctx.fillStyle=this.options.color,this.ctx.beginPath(),this.ctx.arc(0,0,this.strokeWidth,0,2*Math.PI,!0),this.ctx.fill(),this.ctx.beginPath(),this.ctx.moveTo(d,e),this.ctx.lineTo(f,g),this.ctx.lineTo(b,c),this.ctx.fill()},b}(j),c=function(){function a(a){this.elem=a}return a.prototype.updateValues=function(a){return this.value=a[0],this.maxValue=a[1],this.avgValue=a[2],this.render()},a.prototype.render=function(){var a,b;return this.textField&&this.textField.text(m(this.value)),0===this.maxValue&&(this.maxValue=2*this.avgValue),b=this.value/this.maxValue*100,a=this.avgValue/this.maxValue*100,$(".bar-value",this.elem).css({width:b+"%"}),$(".typical-value",this.elem).css({width:a+"%"})},a}(),g=function(a){function b(a){var c,d;this.canvas=a,b.__super__.constructor.call(this),this.percentColors=null,"undefined"!=typeof G_vmlCanvasManager&&(this.canvas=window.G_vmlCanvasManager.initElement(this.canvas)),this.ctx=this.canvas.getContext("2d"),c=this.canvas.clientHeight,d=this.canvas.clientWidth,this.canvas.height=c,this.canvas.width=d,this.gp=[new h(this)],this.setOptions(),this.render()}return r(b,a),b.prototype.elem=null,b.prototype.value=[20],b.prototype.maxValue=80,b.prototype.minValue=0,b.prototype.displayedAngle=0,b.prototype.displayedValue=0,b.prototype.lineWidth=40,b.prototype.paddingTop=.1,b.prototype.paddingBottom=.1,b.prototype.percentColors=null,b.prototype.options={colorStart:"#6fadcf",colorStop:void 0,gradientType:0,strokeColor:"#e0e0e0",pointer:{length:.8,strokeWidth:.035},angle:.15,lineWidth:.44,radiusScale:1,fontSize:40,limitMax:!1,limitMin:!1},b.prototype.setOptions=function(a){var c,d,e,f,g;for(null==a&&(a=null),b.__super__.setOptions.call(this,a),this.configPercentColors(),this.extraPadding=0,this.options.angle<0&&(f=Math.PI*(1+this.options.angle),this.extraPadding=Math.sin(f)),this.availableHeight=this.canvas.height*(1-this.paddingTop-this.paddingBottom),this.lineWidth=this.availableHeight*this.options.lineWidth,this.radius=(this.availableHeight-this.lineWidth/2)/(1+this.extraPadding),this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),g=this.gp,d=0,e=g.length;e>d;d++)c=g[d],c.setOptions(this.options.pointer),c.render();return this},b.prototype.configPercentColors=function(){var a,b,c,d,e,f,g;if(this.percentColors=null,void 0!==this.options.percentColors){for(this.percentColors=new Array,f=[],c=d=0,e=this.options.percentColors.length-1;e>=0?e>=d:d>=e;c=e>=0?++d:--d)g=parseInt(l(this.options.percentColors[c][1]).substring(0,2),16),b=parseInt(l(this.options.percentColors[c][1]).substring(2,4),16),a=parseInt(l(this.options.percentColors[c][1]).substring(4,6),16),f.push(this.percentColors[c]={pct:this.options.percentColors[c][0],color:{r:g,g:b,b:a}});return f}},b.prototype.set=function(a){var b,c,d,e,f,g,i;if(a instanceof Array||(a=[a]),a.length>this.gp.length)for(c=d=0,g=a.length-this.gp.length;g>=0?g>d:d>g;c=g>=0?++d:--d)b=new h(this),b.setOptions(this.options.pointer),this.gp.push(b);else a.length<this.gp.length&&(this.gp=this.gp.slice(this.gp.length-a.length));for(c=0,e=0,f=a.length;f>e;e++)i=a[e],i>this.maxValue?this.options.limitMax?i=this.maxValue:this.maxValue=i+1:i<this.minValue&&(this.options.limitMin?i=this.minValue:this.minValue=i-1),this.gp[c].value=i,this.gp[c++].setOptions({minValue:this.minValue,maxValue:this.maxValue,angle:this.options.angle});return this.value=Math.max(Math.min(a[a.length-1],this.maxValue),this.minValue),AnimationUpdater.run()},b.prototype.getAngle=function(a){return(1+this.options.angle)*Math.PI+(a-this.minValue)/(this.maxValue-this.minValue)*(1-2*this.options.angle)*Math.PI},b.prototype.getColorForPercentage=function(a,b){var c,d,e,f,g,h,i;if(0===a)c=this.percentColors[0].color;else for(c=this.percentColors[this.percentColors.length-1].color,e=f=0,h=this.percentColors.length-1;h>=0?h>=f:f>=h;e=h>=0?++f:--f)if(a<=this.percentColors[e].pct){b===!0?(i=this.percentColors[e-1]||this.percentColors[0],d=this.percentColors[e],g=(a-i.pct)/(d.pct-i.pct),c={r:Math.floor(i.color.r*(1-g)+d.color.r*g),g:Math.floor(i.color.g*(1-g)+d.color.g*g),b:Math.floor(i.color.b*(1-g)+d.color.b*g)}):c=this.percentColors[e].color;break}return"rgb("+[c.r,c.g,c.b].join(",")+")"},b.prototype.getColorForValue=function(a,b){var c;return c=(a-this.minValue)/(this.maxValue-this.minValue),this.getColorForPercentage(c,b)},b.prototype.renderStaticLabels=function(a,b,c,d){var e,f,g,h,i,j,k,l,n,o;for(this.ctx.save(),this.ctx.translate(b,c),e=a.font||"10px Times",j=/\d+\.?\d?/,i=e.match(j)[0],l=e.slice(i.length),f=parseFloat(i)*this.displayScale,this.ctx.font=f+l,this.ctx.fillStyle=a.color||"#000000",this.ctx.textBaseline="bottom",this.ctx.textAlign="center",k=a.labels,g=0,h=k.length;h>g;g++)o=k[g],n=this.getAngle(o)-3*Math.PI/2,this.ctx.rotate(n),this.ctx.fillText(m(o,a.fractionDigits),0,-d-this.lineWidth/2),this.ctx.rotate(-n);return this.ctx.restore()},b.prototype.render=function(){var a,b,c,d,e,f,g,h,i,j,k,l,m;if(l=this.canvas.width/2,d=this.canvas.height*this.paddingTop+this.availableHeight-(this.radius+this.lineWidth/2)*this.extraPadding,a=this.getAngle(this.displayedValue),this.textField&&this.textField.render(this),this.ctx.lineCap="butt",i=this.radius*this.options.radiusScale,this.options.staticLabels&&this.renderStaticLabels(this.options.staticLabels,l,d,i),this.options.staticZones){for(this.ctx.save(),this.ctx.translate(l,d),this.ctx.lineWidth=this.lineWidth,j=this.options.staticZones,e=0,g=j.length;g>e;e++)m=j[e],this.ctx.strokeStyle=m.strokeStyle,this.ctx.beginPath(),this.ctx.arc(0,0,i,this.getAngle(m.min),this.getAngle(m.max),!1),this.ctx.stroke();this.ctx.restore()}else void 0!==this.options.customFillStyle?b=this.options.customFillStyle(this):null!==this.percentColors?b=this.getColorForValue(this.displayedValue,!0):void 0!==this.options.colorStop?(b=0===this.options.gradientType?this.ctx.createRadialGradient(l,d,9,l,d,70):this.ctx.createLinearGradient(0,0,l,0),b.addColorStop(0,this.options.colorStart),b.addColorStop(1,this.options.colorStop)):b=this.options.colorStart,this.ctx.strokeStyle=b,this.ctx.beginPath(),this.ctx.arc(l,d,i,(1+this.options.angle)*Math.PI,a,!1),this.ctx.lineWidth=this.lineWidth,this.ctx.stroke(),this.ctx.strokeStyle=this.options.strokeColor,this.ctx.beginPath(),this.ctx.arc(l,d,i,a,(2-this.options.angle)*Math.PI,!1),this.ctx.stroke();for(this.ctx.translate(l,d),k=this.gp,f=0,h=k.length;h>f;f++)c=k[f],c.update(!0);return this.ctx.translate(-l,-d)},b}(e),d=function(a){function b(a){this.canvas=a,b.__super__.constructor.call(this),"undefined"!=typeof G_vmlCanvasManager&&(this.canvas=window.G_vmlCanvasManager.initElement(this.canvas)),this.ctx=this.canvas.getContext("2d"),this.setOptions(),this.render()}return r(b,a),b.prototype.lineWidth=15,b.prototype.displayedValue=0,b.prototype.value=33,b.prototype.maxValue=80,b.prototype.minValue=0,b.prototype.options={lineWidth:.1,colorStart:"#6f6ea0",colorStop:"#c0c0db",strokeColor:"#eeeeee",shadowColor:"#d5d5d5",angle:.35,radiusScale:1},b.prototype.getAngle=function(a){return(1-this.options.angle)*Math.PI+(a-this.minValue)/(this.maxValue-this.minValue)*(2+this.options.angle-(1-this.options.angle))*Math.PI},b.prototype.setOptions=function(a){return null==a&&(a=null),b.__super__.setOptions.call(this,a),this.lineWidth=this.canvas.height*this.options.lineWidth,this.radius=this.options.radiusScale*(this.canvas.height/2-this.lineWidth/2),this},b.prototype.set=function(a){return this.value=a,this.value>this.maxValue&&(this.maxValue=1.1*this.value),AnimationUpdater.run()},b.prototype.render=function(){var a,b,c,d,e,f;return a=this.getAngle(this.displayedValue),f=this.canvas.width/2,c=this.canvas.height/2,this.textField&&this.textField.render(this),b=this.ctx.createRadialGradient(f,c,39,f,c,70),b.addColorStop(0,this.options.colorStart),b.addColorStop(1,this.options.colorStop),d=this.radius-this.lineWidth/2,e=this.radius+this.lineWidth/2,this.ctx.strokeStyle=this.options.strokeColor,this.ctx.beginPath(),this.ctx.arc(f,c,this.radius,(1-this.options.angle)*Math.PI,(2+this.options.angle)*Math.PI,!1),this.ctx.lineWidth=this.lineWidth,this.ctx.lineCap="round",this.ctx.stroke(),this.ctx.strokeStyle=b,this.ctx.beginPath(),this.ctx.arc(f,c,this.radius,(1-this.options.angle)*Math.PI,a,!1),this.ctx.stroke()},b}(e),f=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return r(b,a),b.prototype.strokeGradient=function(a,b,c,d){var e;return e=this.ctx.createRadialGradient(a,b,c,a,b,d),e.addColorStop(0,this.options.shadowColor),e.addColorStop(.12,this.options._orgStrokeColor),e.addColorStop(.88,this.options._orgStrokeColor),e.addColorStop(1,this.options.shadowColor),e},b.prototype.setOptions=function(a){var c,d,e,f;return null==a&&(a=null),b.__super__.setOptions.call(this,a),f=this.canvas.width/2,c=this.canvas.height/2,d=this.radius-this.lineWidth/2,e=this.radius+this.lineWidth/2,this.options._orgStrokeColor=this.options.strokeColor,this.options.strokeColor=this.strokeGradient(f,c,d,e),this},b}(d),window.AnimationUpdater={elements:[],animId:null,addAll:function(a){var b,c,d,e;for(e=[],c=0,d=a.length;d>c;c++)b=a[c],e.push(AnimationUpdater.elements.push(b));return e},add:function(a){return AnimationUpdater.elements.push(a)},run:function(){var a,b,c,d,e;for(a=!0,e=AnimationUpdater.elements,c=0,d=e.length;d>c;c++)b=e[c],b.update()&&(a=!1);return a?cancelAnimationFrame(AnimationUpdater.animId):AnimationUpdater.animId=requestAnimationFrame(AnimationUpdater.run)}},"function"==typeof window.define&&null!=window.define.amd?define(function(){return{Gauge:g,Donut:f,BaseDonut:d,TextRenderer:i}}):"undefined"!=typeof module&&null!=module.exports?module.exports={Gauge:g,Donut:f,BaseDonut:d,TextRenderer:i}:(window.Gauge=g,window.Donut=f,window.BaseDonut=d,window.TextRenderer=i)}).call(this);
\ No newline at end of file
--- /dev/null
+(function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p=[].slice,q={}.hasOwnProperty,r=function(a,b){function d(){this.constructor=a}for(var c in b)q.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};!function(){var a,b,c,d,e,f,g;for(g=["ms","moz","webkit","o"],c=0,e=g.length;c<e&&(f=g[c],!window.requestAnimationFrame);c++)window.requestAnimationFrame=window[f+"RequestAnimationFrame"],window.cancelAnimationFrame=window[f+"CancelAnimationFrame"]||window[f+"CancelRequestAnimationFrame"];return a=null,d=0,b={},requestAnimationFrame?window.cancelAnimationFrame?void 0:(a=window.requestAnimationFrame,window.requestAnimationFrame=function(c,e){var f;return f=++d,a(function(){if(!b[f])return c()},e),f},window.cancelAnimationFrame=function(a){return b[a]=!0}):(window.requestAnimationFrame=function(a,b){var c,d,e,f;return c=(new Date).getTime(),f=Math.max(0,16-(c-e)),d=window.setTimeout(function(){return a(c+f)},f),e=c+f,d},window.cancelAnimationFrame=function(a){return clearTimeout(a)})}(),String.prototype.hashCode=function(){var a,b,c,d,e;if(b=0,0===this.length)return b;for(c=d=0,e=this.length;0<=e?d<e:d>e;c=0<=e?++d:--d)a=this.charCodeAt(c),b=(b<<5)-b+a,b&=b;return b},o=function(a){var b,c;for(b=Math.floor(a/3600),c=Math.floor((a-3600*b)/60),a-=3600*b+60*c,a+="",c+="";c.length<2;)c="0"+c;for(;a.length<2;)a="0"+a;return b=b?b+":":"",b+c+":"+a},m=function(){var a,b,c;return b=1<=arguments.length?p.call(arguments,0):[],c=b[0],a=b[1],k(c.toFixed(a))},n=function(a,b){var c,d,e;d={};for(c in a)q.call(a,c)&&(e=a[c],d[c]=e);for(c in b)q.call(b,c)&&(e=b[c],d[c]=e);return d},k=function(a){var b,c,d,e;for(a+="",c=a.split("."),d=c[0],e="",c.length>1&&(e="."+c[1]),b=/(\d+)(\d{3})/;b.test(d);)d=d.replace(b,"$1,$2");return d+e},l=function(a){return"#"===a.charAt(0)?a.substring(1,7):a},j=function(){function a(a,b){null==a&&(a=!0),this.clear=null==b||b,a&&AnimationUpdater.add(this)}return a.prototype.animationSpeed=32,a.prototype.update=function(a){var b;return null==a&&(a=!1),!(!a&&this.displayedValue===this.value)&&(this.ctx&&this.clear&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),b=this.value-this.displayedValue,Math.abs(b/this.animationSpeed)<=.001?this.displayedValue=this.value:this.displayedValue=this.displayedValue+b/this.animationSpeed,this.render(),!0)},a}(),e=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return r(b,a),b.prototype.displayScale=1,b.prototype.setTextField=function(a,b){return this.textField=a instanceof i?a:new i(a,b)},b.prototype.setMinValue=function(a,b){var c,d,e,f,g;if(this.minValue=a,null==b&&(b=!0),b){for(this.displayedValue=this.minValue,f=this.gp||[],g=[],d=0,e=f.length;d<e;d++)c=f[d],g.push(c.displayedValue=this.minValue);return g}},b.prototype.setOptions=function(a){return null==a&&(a=null),this.options=n(this.options,a),this.textField&&(this.textField.el.style.fontSize=a.fontSize+"px"),this.options.angle>.5&&(this.options.angle=.5),this.configDisplayScale(),this},b.prototype.configDisplayScale=function(){var a,b,c,d,e;return d=this.displayScale,this.options.highDpiSupport===!1?delete this.displayScale:(b=window.devicePixelRatio||1,a=this.ctx.webkitBackingStorePixelRatio||this.ctx.mozBackingStorePixelRatio||this.ctx.msBackingStorePixelRatio||this.ctx.oBackingStorePixelRatio||this.ctx.backingStorePixelRatio||1,this.displayScale=b/a),this.displayScale!==d&&(e=this.canvas.G__width||this.canvas.width,c=this.canvas.G__height||this.canvas.height,this.canvas.width=e*this.displayScale,this.canvas.height=c*this.displayScale,this.canvas.style.width=e+"px",this.canvas.style.height=c+"px",this.canvas.G__width=e,this.canvas.G__height=c),this},b}(j),i=function(){function a(a,b){this.el=a,this.fractionDigits=b}return a.prototype.render=function(a){return this.el.innerHTML=m(a.displayedValue,this.fractionDigits)},a}(),a=function(a){function b(a,b){this.elem=a,this.text=null!=b&&b,this.value=1*this.elem.innerHTML,this.text&&(this.value=0)}return r(b,a),b.prototype.displayedValue=0,b.prototype.value=0,b.prototype.setVal=function(a){return this.value=1*a},b.prototype.render=function(){var a;return a=this.text?o(this.displayedValue.toFixed(0)):k(m(this.displayedValue)),this.elem.innerHTML=a},b}(j),b={create:function(b){var c,d,e,f;for(f=[],d=0,e=b.length;d<e;d++)c=b[d],f.push(new a(c));return f}},h=function(a){function b(a){this.gauge=a,this.ctx=this.gauge.ctx,this.canvas=this.gauge.canvas,b.__super__.constructor.call(this,!1,!1),this.setOptions()}return r(b,a),b.prototype.displayedValue=0,b.prototype.value=0,b.prototype.options={strokeWidth:.035,length:.1,color:"#000000"},b.prototype.setOptions=function(a){return null==a&&(a=null),this.options=n(this.options,a),this.length=2*this.gauge.radius*this.gauge.options.radiusScale*this.options.length,this.strokeWidth=this.canvas.height*this.options.strokeWidth,this.maxValue=this.gauge.maxValue,this.minValue=this.gauge.minValue,this.animationSpeed=this.gauge.animationSpeed,this.options.angle=this.gauge.options.angle},b.prototype.render=function(){var a,b,c,d,e,f,g;return a=this.gauge.getAngle.call(this,this.displayedValue),f=Math.round(this.length*Math.cos(a)),g=Math.round(this.length*Math.sin(a)),d=Math.round(this.strokeWidth*Math.cos(a-Math.PI/2)),e=Math.round(this.strokeWidth*Math.sin(a-Math.PI/2)),b=Math.round(this.strokeWidth*Math.cos(a+Math.PI/2)),c=Math.round(this.strokeWidth*Math.sin(a+Math.PI/2)),this.ctx.fillStyle=this.options.color,this.ctx.beginPath(),this.ctx.arc(0,0,this.strokeWidth,0,2*Math.PI,!0),this.ctx.fill(),this.ctx.beginPath(),this.ctx.moveTo(d,e),this.ctx.lineTo(f,g),this.ctx.lineTo(b,c),this.ctx.fill()},b}(j),c=function(){function a(a){this.elem=a}return a.prototype.updateValues=function(a){return this.value=a[0],this.maxValue=a[1],this.avgValue=a[2],this.render()},a.prototype.render=function(){var a,b;return this.textField&&this.textField.text(m(this.value)),0===this.maxValue&&(this.maxValue=2*this.avgValue),b=this.value/this.maxValue*100,a=this.avgValue/this.maxValue*100,$(".bar-value",this.elem).css({width:b+"%"}),$(".typical-value",this.elem).css({width:a+"%"})},a}(),g=function(a){function b(a){var c,d;this.canvas=a,b.__super__.constructor.call(this),this.percentColors=null,this.forceUpdate=!0,"undefined"!=typeof G_vmlCanvasManager&&(this.canvas=window.G_vmlCanvasManager.initElement(this.canvas)),this.ctx=this.canvas.getContext("2d"),c=this.canvas.clientHeight,d=this.canvas.clientWidth,this.canvas.height=c,this.canvas.width=d,this.gp=[new h(this)],this.setOptions(),this.render()}return r(b,a),b.prototype.elem=null,b.prototype.value=[20],b.prototype.maxValue=80,b.prototype.minValue=0,b.prototype.displayedAngle=0,b.prototype.displayedValue=0,b.prototype.lineWidth=40,b.prototype.paddingTop=.1,b.prototype.paddingBottom=.1,b.prototype.percentColors=null,b.prototype.options={colorStart:"#6fadcf",colorStop:void 0,gradientType:0,strokeColor:"#e0e0e0",pointer:{length:.8,strokeWidth:.035},angle:.15,lineWidth:.44,radiusScale:1,fontSize:40,limitMax:!1,limitMin:!1},b.prototype.setOptions=function(a){var c,d,e,f,g;for(null==a&&(a=null),b.__super__.setOptions.call(this,a),this.configPercentColors(),this.extraPadding=0,this.options.angle<0&&(f=Math.PI*(1+this.options.angle),this.extraPadding=Math.sin(f)),this.availableHeight=this.canvas.height*(1-this.paddingTop-this.paddingBottom),this.lineWidth=this.availableHeight*this.options.lineWidth,this.radius=(this.availableHeight-this.lineWidth/2)/(1+this.extraPadding),this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),g=this.gp,d=0,e=g.length;d<e;d++)c=g[d],c.setOptions(this.options.pointer),c.render();return this},b.prototype.configPercentColors=function(){var a,b,c,d,e,f,g;if(this.percentColors=null,void 0!==this.options.percentColors){for(this.percentColors=new Array,f=[],c=d=0,e=this.options.percentColors.length-1;0<=e?d<=e:d>=e;c=0<=e?++d:--d)g=parseInt(l(this.options.percentColors[c][1]).substring(0,2),16),b=parseInt(l(this.options.percentColors[c][1]).substring(2,4),16),a=parseInt(l(this.options.percentColors[c][1]).substring(4,6),16),f.push(this.percentColors[c]={pct:this.options.percentColors[c][0],color:{r:g,g:b,b:a}});return f}},b.prototype.set=function(a){var b,c,d,e,f,g,i;if(a instanceof Array||(a=[a]),a.length>this.gp.length)for(c=d=0,g=a.length-this.gp.length;0<=g?d<g:d>g;c=0<=g?++d:--d)b=new h(this),b.setOptions(this.options.pointer),this.gp.push(b);else a.length<this.gp.length&&(this.gp=this.gp.slice(this.gp.length-a.length));for(c=0,e=0,f=a.length;e<f;e++)i=a[e],i>this.maxValue?this.options.limitMax?i=this.maxValue:this.maxValue=i+1:i<this.minValue&&(this.options.limitMin?i=this.minValue:this.minValue=i-1),this.gp[c].value=i,this.gp[c++].setOptions({minValue:this.minValue,maxValue:this.maxValue,angle:this.options.angle});return this.value=Math.max(Math.min(a[a.length-1],this.maxValue),this.minValue),AnimationUpdater.run(this.forceUpdate),this.forceUpdate=!1},b.prototype.getAngle=function(a){return(1+this.options.angle)*Math.PI+(a-this.minValue)/(this.maxValue-this.minValue)*(1-2*this.options.angle)*Math.PI},b.prototype.getColorForPercentage=function(a,b){var c,d,e,f,g,h,i;if(0===a)c=this.percentColors[0].color;else for(c=this.percentColors[this.percentColors.length-1].color,e=f=0,h=this.percentColors.length-1;0<=h?f<=h:f>=h;e=0<=h?++f:--f)if(a<=this.percentColors[e].pct){b===!0?(i=this.percentColors[e-1]||this.percentColors[0],d=this.percentColors[e],g=(a-i.pct)/(d.pct-i.pct),c={r:Math.floor(i.color.r*(1-g)+d.color.r*g),g:Math.floor(i.color.g*(1-g)+d.color.g*g),b:Math.floor(i.color.b*(1-g)+d.color.b*g)}):c=this.percentColors[e].color;break}return"rgb("+[c.r,c.g,c.b].join(",")+")"},b.prototype.getColorForValue=function(a,b){var c;return c=(a-this.minValue)/(this.maxValue-this.minValue),this.getColorForPercentage(c,b)},b.prototype.renderStaticLabels=function(a,b,c,d){var e,f,g,h,i,j,k,l,n,o;for(this.ctx.save(),this.ctx.translate(b,c),e=a.font||"10px Times",j=/\d+\.?\d?/,i=e.match(j)[0],l=e.slice(i.length),f=parseFloat(i)*this.displayScale,this.ctx.font=f+l,this.ctx.fillStyle=a.color||"#000000",this.ctx.textBaseline="bottom",this.ctx.textAlign="center",k=a.labels,g=0,h=k.length;g<h;g++)o=k[g],(!this.options.limitMin||o>=this.minValue)&&(!this.options.limitMax||o<=this.maxValue)&&(n=this.getAngle(o)-3*Math.PI/2,this.ctx.rotate(n),this.ctx.fillText(m(o,a.fractionDigits),0,-d-this.lineWidth/2),this.ctx.rotate(-n));return this.ctx.restore()},b.prototype.render=function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o;if(n=this.canvas.width/2,d=this.canvas.height*this.paddingTop+this.availableHeight-(this.radius+this.lineWidth/2)*this.extraPadding,a=this.getAngle(this.displayedValue),this.textField&&this.textField.render(this),this.ctx.lineCap="butt",k=this.radius*this.options.radiusScale,this.options.staticLabels&&this.renderStaticLabels(this.options.staticLabels,n,d,k),this.options.staticZones){for(this.ctx.save(),this.ctx.translate(n,d),this.ctx.lineWidth=this.lineWidth,l=this.options.staticZones,e=0,g=l.length;e<g;e++)o=l[e],j=o.min,this.options.limitMin&&j<this.minValue&&(j=this.minValue),i=o.max,this.options.limitMax&&i>this.maxValue&&(i=this.maxValue),this.ctx.strokeStyle=o.strokeStyle,this.ctx.beginPath(),this.ctx.arc(0,0,k,this.getAngle(j),this.getAngle(i),!1),this.ctx.stroke();this.ctx.restore()}else void 0!==this.options.customFillStyle?b=this.options.customFillStyle(this):null!==this.percentColors?b=this.getColorForValue(this.displayedValue,!0):void 0!==this.options.colorStop?(b=0===this.options.gradientType?this.ctx.createRadialGradient(n,d,9,n,d,70):this.ctx.createLinearGradient(0,0,n,0),b.addColorStop(0,this.options.colorStart),b.addColorStop(1,this.options.colorStop)):b=this.options.colorStart,this.ctx.strokeStyle=b,this.ctx.beginPath(),this.ctx.arc(n,d,k,(1+this.options.angle)*Math.PI,a,!1),this.ctx.lineWidth=this.lineWidth,this.ctx.stroke(),this.ctx.strokeStyle=this.options.strokeColor,this.ctx.beginPath(),this.ctx.arc(n,d,k,a,(2-this.options.angle)*Math.PI,!1),this.ctx.stroke();for(this.ctx.translate(n,d),m=this.gp,f=0,h=m.length;f<h;f++)c=m[f],c.update(!0);return this.ctx.translate(-n,-d)},b}(e),d=function(a){function b(a){this.canvas=a,b.__super__.constructor.call(this),"undefined"!=typeof G_vmlCanvasManager&&(this.canvas=window.G_vmlCanvasManager.initElement(this.canvas)),this.ctx=this.canvas.getContext("2d"),this.setOptions(),this.render()}return r(b,a),b.prototype.lineWidth=15,b.prototype.displayedValue=0,b.prototype.value=33,b.prototype.maxValue=80,b.prototype.minValue=0,b.prototype.options={lineWidth:.1,colorStart:"#6f6ea0",colorStop:"#c0c0db",strokeColor:"#eeeeee",shadowColor:"#d5d5d5",angle:.35,radiusScale:1},b.prototype.getAngle=function(a){return(1-this.options.angle)*Math.PI+(a-this.minValue)/(this.maxValue-this.minValue)*(2+this.options.angle-(1-this.options.angle))*Math.PI},b.prototype.setOptions=function(a){return null==a&&(a=null),b.__super__.setOptions.call(this,a),this.lineWidth=this.canvas.height*this.options.lineWidth,this.radius=this.options.radiusScale*(this.canvas.height/2-this.lineWidth/2),this},b.prototype.set=function(a){return this.value=a,this.value>this.maxValue&&(this.maxValue=1.1*this.value),AnimationUpdater.run()},b.prototype.render=function(){var a,b,c,d,e,f;return a=this.getAngle(this.displayedValue),f=this.canvas.width/2,c=this.canvas.height/2,this.textField&&this.textField.render(this),b=this.ctx.createRadialGradient(f,c,39,f,c,70),b.addColorStop(0,this.options.colorStart),b.addColorStop(1,this.options.colorStop),d=this.radius-this.lineWidth/2,e=this.radius+this.lineWidth/2,this.ctx.strokeStyle=this.options.strokeColor,this.ctx.beginPath(),this.ctx.arc(f,c,this.radius,(1-this.options.angle)*Math.PI,(2+this.options.angle)*Math.PI,!1),this.ctx.lineWidth=this.lineWidth,this.ctx.lineCap="round",this.ctx.stroke(),this.ctx.strokeStyle=b,this.ctx.beginPath(),this.ctx.arc(f,c,this.radius,(1-this.options.angle)*Math.PI,a,!1),this.ctx.stroke()},b}(e),f=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}return r(b,a),b.prototype.strokeGradient=function(a,b,c,d){var e;return e=this.ctx.createRadialGradient(a,b,c,a,b,d),e.addColorStop(0,this.options.shadowColor),e.addColorStop(.12,this.options._orgStrokeColor),e.addColorStop(.88,this.options._orgStrokeColor),e.addColorStop(1,this.options.shadowColor),e},b.prototype.setOptions=function(a){var c,d,e,f;return null==a&&(a=null),b.__super__.setOptions.call(this,a),f=this.canvas.width/2,c=this.canvas.height/2,d=this.radius-this.lineWidth/2,e=this.radius+this.lineWidth/2,this.options._orgStrokeColor=this.options.strokeColor,this.options.strokeColor=this.strokeGradient(f,c,d,e),this},b}(d),window.AnimationUpdater={elements:[],animId:null,addAll:function(a){var b,c,d,e;for(e=[],c=0,d=a.length;c<d;c++)b=a[c],e.push(AnimationUpdater.elements.push(b));return e},add:function(a){return AnimationUpdater.elements.push(a)},run:function(a){var b,c,d,e,f;for(null==a&&(a=!1),b=!0,f=AnimationUpdater.elements,d=0,e=f.length;d<e;d++)c=f[d],c.update(a===!0)&&(b=!1);return b?cancelAnimationFrame(AnimationUpdater.animId):AnimationUpdater.animId=requestAnimationFrame(AnimationUpdater.run)}},"function"==typeof window.define&&null!=window.define.amd?define(function(){return{Gauge:g,Donut:f,BaseDonut:d,TextRenderer:i}}):"undefined"!=typeof module&&null!=module.exports?module.exports={Gauge:g,Donut:f,BaseDonut:d,TextRenderer:i}:(window.Gauge=g,window.Donut=f,window.BaseDonut=d,window.TextRenderer=i)}).call(this);
\ No newline at end of file