]> arthur.barton.de Git - netdata.git/commitdiff
Merge pull request #1740 from simonnagl/feature/version
authorCosta Tsaousis <costa@tsaousis.gr>
Sat, 11 Feb 2017 23:32:16 +0000 (01:32 +0200)
committerGitHub <noreply@github.com>
Sat, 11 Feb 2017 23:32:16 +0000 (01:32 +0200)
Add version to /api/v1/charts

21 files changed:
charts.d/nut.chart.sh
conf.d/Makefile.am
conf.d/charts.d/nut.conf
conf.d/health.d/web_log.conf [new file with mode: 0644]
conf.d/python.d.conf
conf.d/python.d/web_log.conf [new file with mode: 0644]
configs.signatures
netdata-installer.sh
plugins.d/python.d.plugin
python.d/Makefile.am
python.d/gunicorn_log.chart.py [deleted file]
python.d/nginx_log.chart.py [deleted file]
python.d/web_log.chart.py [new file with mode: 0644]
src/health.c
src/rrd.c
web/Makefile.am
web/dashboard.js
web/dashboard_info.js
web/index.html
web/lib/gauge-1.3.1.min.js [deleted file]
web/lib/gauge-1.3.2.min.js [new file with mode: 0644]

index e0b1b4cf981bedbb6e0a8de8c59cecb414d1c77d..6137639f982621277caa6fe21fce46133ef400eb 100644 (file)
@@ -2,7 +2,7 @@
 
 # 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+
 #
 
@@ -13,8 +13,13 @@ nut_ups=
 # 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
 
@@ -26,6 +31,12 @@ nut_get_all() {
 
 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() {
@@ -97,6 +108,15 @@ DIMENSION load load absolute 1 100
 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
@@ -131,6 +151,8 @@ BEGIN {
        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 };
@@ -146,6 +168,7 @@ BEGIN {
 /^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;
@@ -184,6 +207,12 @@ END {
        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
index 7b364a62319be6a410531847aa2160aba8ac44c1..c746a909d56e1b6994201238e22dbf375047a837 100644 (file)
@@ -33,7 +33,6 @@ dist_pythonconfig_DATA = \
     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 \
@@ -42,7 +41,6 @@ dist_pythonconfig_DATA = \
     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 \
@@ -54,6 +52,7 @@ dist_pythonconfig_DATA = \
     python.d/smartd_log.conf \
     python.d/tomcat.conf \
     python.d/varnish.conf \
+    python.d/web_log.conf \
     $(NULL)
 
 healthconfigdir=$(configdir)/health.d
@@ -77,6 +76,7 @@ dist_healthconfig_DATA = \
     health.d/retroshare.conf \
     health.d/squid.conf \
     health.d/varnish.conf \
+    health.d/web_log.conf \
     $(NULL)
 
 if LINUX
index 2844849de0fbfc2622a3b11dd4b7c494ea30e04f..a836692d89cb134a0fabb68a18e59954233fbfb3 100644 (file)
@@ -2,7 +2,7 @@
 
 # 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
diff --git a/conf.d/health.d/web_log.conf b/conf.d/health.d/web_log.conf
new file mode 100644 (file)
index 0000000..c8d0c11
--- /dev/null
@@ -0,0 +1,169 @@
+
+# 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
+
index 7e4fa801f49ec75844256771bdc22593515392cb..96cb4d831f1e58791e0e3e9ecdbf2d26669697a3 100644 (file)
@@ -22,24 +22,45 @@ log_interval: 3600
 # 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
diff --git a/conf.d/python.d/web_log.conf b/conf.d/python.d/web_log.conf
new file mode 100644 (file)
index 0000000..ff993dc
--- /dev/null
@@ -0,0 +1,140 @@
+# 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'
index 2149d7226b55121d5e1a0d1a38dd88186cf1ebbf..a56a63deb72b17eb554fe8fe4ee21f8bf9443698 100644 (file)
@@ -1,6 +1,7 @@
 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'
@@ -9,6 +10,8 @@ declare -A configs_signatures=(
   ['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'
@@ -145,10 +148,12 @@ declare -A configs_signatures=(
   ['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'
@@ -157,6 +162,7 @@ declare -A configs_signatures=(
   ['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'
@@ -173,10 +179,12 @@ declare -A configs_signatures=(
   ['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'
@@ -236,6 +244,7 @@ declare -A configs_signatures=(
   ['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'
@@ -347,6 +356,7 @@ declare -A configs_signatures=(
   ['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'
@@ -357,6 +367,7 @@ declare -A configs_signatures=(
   ['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'
@@ -365,12 +376,14 @@ declare -A configs_signatures=(
   ['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'
index 4e1847fa3cd1c41974b0b6a659ea91015d7416ca..606b6c7650bc9164dd454d96c915ede12c73badb 100755 (executable)
@@ -707,6 +707,7 @@ NETDATA_ADDED_TO_DOCKER=0
 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
@@ -715,6 +716,7 @@ if [ ${UID} -eq 0 ]
     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
@@ -1381,6 +1383,15 @@ if [ $? -eq 0 -a "${NETDATA_ADDED_TO_HAPROXY}" = "1" ]
     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
index 44b729094ce307189f3fcecbdfc32464c37d1b60..126041ab13a55df1bc6a2b959c42e10e8d200e28 100755 (executable)
@@ -500,7 +500,7 @@ def run():
     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)
index b6ed315742062146143e4df62779fde5066dcf28..d0c581654e49aebd6aa611d8087ceecebf2ef120 100644 (file)
@@ -19,7 +19,6 @@ dist_python_SCRIPTS = \
     exim.chart.py \
     fail2ban.chart.py \
     freeradius.chart.py \
-    gunicorn_log.chart.py \
     haproxy.chart.py \
     hddtemp.chart.py \
     ipfs.chart.py \
@@ -28,7 +27,6 @@ dist_python_SCRIPTS = \
     memcached.chart.py \
     mysql.chart.py \
     nginx.chart.py \
-    nginx_log.chart.py \
     ovpn_status_log.chart.py \
     phpfpm.chart.py \
     postfix.chart.py \
@@ -40,6 +38,7 @@ dist_python_SCRIPTS = \
     smartd_log.chart.py \
     tomcat.chart.py \
     varnish.chart.py \
+    web_log.chart.py \
     python-modules-installer.sh \
     $(NULL)
 
diff --git a/python.d/gunicorn_log.chart.py b/python.d/gunicorn_log.chart.py
deleted file mode 100644 (file)
index 9459636..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-# -*- 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
diff --git a/python.d/nginx_log.chart.py b/python.d/nginx_log.chart.py
deleted file mode 100644 (file)
index ef964a5..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-# -*- 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
diff --git a/python.d/web_log.chart.py b/python.d/web_log.chart.py
new file mode 100644 (file)
index 0000000..d706134
--- /dev/null
@@ -0,0 +1,381 @@
+# -*- 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
index a3a846004206288d7fc481784a0930b41809dcfb..ffe4713b2fbead7096b7213f2191fc32be533037 100644 (file)
@@ -1896,14 +1896,35 @@ static inline int health_parse_db_lookup(
     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) {
@@ -2003,8 +2024,8 @@ int health_readfile(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);
@@ -2030,7 +2051,7 @@ int health_readfile(const char *path, const char *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;
@@ -2053,7 +2074,7 @@ int health_readfile(const char *path, const char *filename) {
                 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;
@@ -2072,7 +2093,7 @@ int health_readfile(const char *path, const char *filename) {
 
                     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)) {
@@ -2136,7 +2157,7 @@ int health_readfile(const char *path, const char *filename) {
 
                     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) {
@@ -2146,7 +2167,7 @@ int health_readfile(const char *path, const char *filename) {
 
                     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) {
@@ -2156,7 +2177,7 @@ int health_readfile(const char *path, const char *filename) {
 
                     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)) {
@@ -2167,7 +2188,7 @@ int health_readfile(const char *path, const char *filename) {
 
                     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)) {
@@ -2190,14 +2211,14 @@ int health_readfile(const char *path, const char *filename) {
 
                     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)) {
@@ -2260,7 +2281,7 @@ int health_readfile(const char *path, const char *filename) {
 
                     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) {
@@ -2270,7 +2291,7 @@ int health_readfile(const char *path, const char *filename) {
 
                     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) {
@@ -2280,7 +2301,7 @@ int health_readfile(const char *path, const char *filename) {
 
                     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)) {
@@ -2291,7 +2312,7 @@ int health_readfile(const char *path, const char *filename) {
 
                     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)) {
index 17774ba2d9a39be1883fba4490ab114acd2c7fee..a6c5cbb6a3c4d54dfba9e8ea03bc2331548f4115 100644 (file)
--- a/src/rrd.c
+++ b/src/rrd.c
@@ -302,9 +302,9 @@ int rrd_memory_mode_id(const char *name)
 
 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;
 }
@@ -692,11 +692,6 @@ RRDDIM *rrddim_add(RRDSET *st, const char *id, const char *name, long multiplier
             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);
@@ -714,6 +709,9 @@ RRDDIM *rrddim_add(RRDSET *st, const char *id, const char *name, long multiplier
             // 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) {
index 5dde096861d1d833e8b282a71e62718dd64ed334..6d1130239fc259770bd880ef1fe885727194d115 100644 (file)
@@ -44,7 +44,7 @@ dist_weblib_DATA = \
        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 \
index c134581959dc94cc762509d6444d0c2b15c2de0f..9078b26e4fdbd983712033f27094c93e7e27362d 100644 (file)
@@ -130,7 +130,7 @@ var NETDATA = window.NETDATA || {};
     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';
index 173e95bb921f016be6be9999bf084aa188beffd1..b4028168e8c072c50d16760a35bb59c2b5e879c0 100644 (file)
@@ -1,7 +1,9 @@
 
 var netdataDashboard = window.netdataDashboard || {};
 
-// menu
+// ----------------------------------------------------------------------------
+// menus
+
 // information about the main menus
 
 netdataDashboard.menu = {
@@ -26,7 +28,7 @@ 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': {
@@ -217,6 +219,12 @@ netdataDashboard.menu = {
         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>',
@@ -254,9 +262,31 @@ netdataDashboard.menu = {
     }
 };
 
-// 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.'
@@ -306,7 +336,11 @@ netdataDashboard.submenu = {
     }
 };
 
+
+
+// ----------------------------------------------------------------------------
 // chart
+
 // information works on the context of a chart
 // Its purpose is to set:
 //
@@ -811,6 +845,121 @@ netdataDashboard.context = {
 
     '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>).'
     }
 
 };
index 72b39cbcf385cbce11236404e8b93edc2e913619..e8e821f43193fbba30f396a505bfacf2d00c0c3e 100644 (file)
 
             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>
diff --git a/web/lib/gauge-1.3.1.min.js b/web/lib/gauge-1.3.1.min.js
deleted file mode 100644 (file)
index 15801e9..0000000
+++ /dev/null
@@ -1 +0,0 @@
-(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
diff --git a/web/lib/gauge-1.3.2.min.js b/web/lib/gauge-1.3.2.min.js
new file mode 100644 (file)
index 0000000..be327fe
--- /dev/null
@@ -0,0 +1 @@
+(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