├── debian ├── compat ├── dirs ├── nginx-nr-agent.default ├── nginx-nr-agent.logrotate ├── postinst ├── control ├── preinst ├── nginx-nr-agent.ini ├── rules ├── copyright ├── changelog └── init.d ├── .gitignore ├── rpm ├── SOURCES │ ├── nginx-nr-agent.sysconfig │ ├── nginx-nr-agent.logrotate │ ├── nginx-nr-agent.ini │ ├── COPYRIGHT │ └── nginx-nr-agent.init └── SPECS │ └── nginx-nr-agent.spec ├── Makefile ├── LICENSE ├── README.md └── nginx-nr-agent.py /debian/compat: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /debian/dirs: -------------------------------------------------------------------------------- 1 | /etc/nginx-nr-agent 2 | /usr/bin 3 | /usr/share/nginx-nr-agent 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build_output/ 2 | debian/nginx-nr-agent.py 3 | rpm/SOURCES/nginx-nr-agent.py 4 | *.deb 5 | *.rpm 6 | -------------------------------------------------------------------------------- /rpm/SOURCES/nginx-nr-agent.sysconfig: -------------------------------------------------------------------------------- 1 | # Configuration file for the nginx-nr-agent service. 2 | 3 | export HTTPS_PROXY="" 4 | -------------------------------------------------------------------------------- /debian/nginx-nr-agent.default: -------------------------------------------------------------------------------- 1 | # Defaults for nginx-nr-agent initscript 2 | # sourced by /etc/init.d/nginx-nr-agent 3 | 4 | export HTTPS_PROXY="" 5 | 6 | -------------------------------------------------------------------------------- /debian/nginx-nr-agent.logrotate: -------------------------------------------------------------------------------- 1 | /var/log/nginx-nr-agent.log { 2 | compress 3 | copytruncate 4 | missingok 5 | rotate 3 6 | size 5M 7 | } 8 | -------------------------------------------------------------------------------- /rpm/SOURCES/nginx-nr-agent.logrotate: -------------------------------------------------------------------------------- 1 | /var/log/nginx-nr-agent.log { 2 | compress 3 | copytruncate 4 | missingok 5 | rotate 3 6 | size 5M 7 | } 8 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ "$1" != "configure" ]; then 6 | exit 0 7 | fi 8 | 9 | if [ -z "$2" ]; then 10 | update-rc.d nginx-nr-agent defaults >/dev/null 11 | mkdir -p /var/run/nginx-nr-agent 12 | touch /var/log/nginx-nr-agent.log 13 | chown nobody /var/run/nginx-nr-agent /var/log/nginx-nr-agent.log 14 | fi 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | debian: 2 | cp nginx-nr-agent.py debian/ 3 | dpkg-buildpackage 4 | mkdir -p build_output/ 5 | mv ../nginx-nr-agent*.deb build_output/ 6 | mv ../nginx-nr-agent*.tar.gz build_output/ 7 | rm -f ../nginx-nr-agent* 8 | 9 | rpm: 10 | cp nginx-nr-agent.py rpm/SOURCES/ 11 | mkdir -p ~/rpmbuild/ 12 | cp -r rpm/* ~/rpmbuild/ 13 | rpmbuild -bb ~/rpmbuild/SPECS/nginx-nr-agent.spec 14 | mkdir -p build_output/ 15 | mv ~/rpmbuild/RPMS/noarch/nginx-nr-agent*.rpm build_output/ 16 | 17 | clean: 18 | rm -rf build_output/ 19 | rm -rf ~/rpmbuild/ 20 | 21 | .PHONY: debian rpm 22 | 23 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: nginx-nr-agent 2 | Section: misc 3 | Priority: optional 4 | Maintainer: Andrei Belov 5 | Build-Depends: debhelper (>> 5.0.0) 6 | Standards-Version: 3.8.2 7 | Homepage: https://www.nginx.com/ 8 | 9 | Package: nginx-nr-agent 10 | Architecture: all 11 | Depends: ${misc:Depends}, lsb-base, python, python-daemon, python-setproctitle 12 | Description: New Relic agent for NGINX and NGINX Plus 13 | This package contains agent script used for collecting 14 | and reporting a number of metrics from NGINX and/or NGINX Plus 15 | instances to New Relic. 16 | -------------------------------------------------------------------------------- /debian/preinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | case "$1" in 6 | install) 7 | cat <&2 34 | exit 0 35 | ;; 36 | esac 37 | 38 | #DEBHELPER# 39 | 40 | exit 0 41 | -------------------------------------------------------------------------------- /debian/nginx-nr-agent.ini: -------------------------------------------------------------------------------- 1 | # global settings 2 | 3 | [global] 4 | newrelic_license_key=YOUR_LICENSE_KEY_HERE 5 | poll_interval=60 6 | 7 | # logging settings 8 | 9 | [loggers] 10 | keys=root 11 | 12 | [handlers] 13 | keys=consoleHandler,fileHandler 14 | 15 | [formatters] 16 | keys=simpleFormatter 17 | 18 | [logger_root] 19 | level=DEBUG 20 | handlers=consoleHandler,fileHandler 21 | 22 | [handler_consoleHandler] 23 | class=StreamHandler 24 | level=DEBUG 25 | formatter=simpleFormatter 26 | args=(sys.stdout,) 27 | 28 | [handler_fileHandler] 29 | class=FileHandler 30 | level=DEBUG 31 | formatter=simpleFormatter 32 | args=('/var/log/nginx-nr-agent.log','a',) 33 | 34 | [formatter_simpleFormatter] 35 | format=%(asctime)s %(name)s [%(levelname)s]: %(message)s 36 | datefmt= 37 | 38 | # data sources settings 39 | 40 | #[source1] 41 | #name=exampleorg 42 | #url=http://example.org/status 43 | # 44 | #[source2] 45 | #name=examplecom 46 | #url=http://example.com/api 47 | #http_user=testuser 48 | #http_pass=testpass 49 | -------------------------------------------------------------------------------- /rpm/SOURCES/nginx-nr-agent.ini: -------------------------------------------------------------------------------- 1 | # global settings 2 | 3 | [global] 4 | newrelic_license_key=YOUR_LICENSE_KEY_HERE 5 | poll_interval=60 6 | 7 | # logging settings 8 | 9 | [loggers] 10 | keys=root 11 | 12 | [handlers] 13 | keys=consoleHandler,fileHandler 14 | 15 | [formatters] 16 | keys=simpleFormatter 17 | 18 | [logger_root] 19 | level=DEBUG 20 | handlers=consoleHandler,fileHandler 21 | 22 | [handler_consoleHandler] 23 | class=StreamHandler 24 | level=DEBUG 25 | formatter=simpleFormatter 26 | args=(sys.stdout,) 27 | 28 | [handler_fileHandler] 29 | class=FileHandler 30 | level=DEBUG 31 | formatter=simpleFormatter 32 | args=('/var/log/nginx-nr-agent.log','a',) 33 | 34 | [formatter_simpleFormatter] 35 | format=%(asctime)s %(name)s [%(levelname)s]: %(message)s 36 | datefmt= 37 | 38 | # data sources settings 39 | 40 | #[source1] 41 | #name=exampleorg 42 | #url=http://example.org/status 43 | # 44 | #[source2] 45 | #name=examplecom 46 | #url=http://example.com/api 47 | #http_user=testuser 48 | #http_pass=testpass 49 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # Uncomment this to turn on verbose mode. 4 | #export DH_VERBOSE=1 5 | 6 | configure: configure-stamp 7 | configure-stamp: 8 | dh_testdir 9 | touch configure-stamp 10 | 11 | build: configure-stamp build-stamp 12 | build-stamp: 13 | dh_testdir 14 | touch build-stamp 15 | 16 | clean: 17 | dh_testdir 18 | dh_testroot 19 | rm -f build-stamp configure-stamp 20 | dh_clean 21 | 22 | install: build 23 | dh_testdir 24 | dh_testroot 25 | dh_prep 26 | dh_installdirs 27 | dh_install 28 | dh_installinit 29 | dh_installlogrotate 30 | install -m 755 debian/nginx-nr-agent.py debian/nginx-nr-agent/usr/bin/ 31 | install -m 644 debian/nginx-nr-agent.ini debian/nginx-nr-agent/etc/nginx-nr-agent/ 32 | 33 | binary-indep: build install 34 | dh_testdir 35 | dh_testroot 36 | dh_installdocs 37 | dh_installchangelogs 38 | dh_link 39 | dh_strip 40 | dh_compress -XREADME 41 | dh_fixperms 42 | dh_installdeb 43 | dh_perl 44 | dh_gencontrol 45 | dh_md5sums 46 | dh_builddeb 47 | 48 | binary-arch: build install 49 | 50 | binary: binary-indep binary-arch 51 | .PHONY: build clean binary-indep binary-arch binary install configure 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011-2019 Nginx, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * 1. Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 2. Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 15 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 18 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 23 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | * SUCH DAMAGE. 25 | */ 26 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2002-2019 Igor Sysoev 3 | * Copyright (C) 2011-2019 Nginx, Inc. 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 1. Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * 2. Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 16 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 21 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 22 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 23 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 24 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 | * SUCH DAMAGE. 26 | */ 27 | -------------------------------------------------------------------------------- /rpm/SOURCES/COPYRIGHT: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2002-2019 Igor Sysoev 3 | * Copyright (C) 2011-2019 Nginx, Inc. 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 1. Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * 2. Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 16 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 21 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 22 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 23 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 24 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 | * SUCH DAMAGE. 26 | */ 27 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | nginx-nr-agent (2.0.1-1) unstable; urgency=low 2 | 3 | * 2.0.1-1 4 | * legacy status module support removed 5 | * new nginx-plus API support added 6 | 7 | -- Andrei Belov Wed, 8 Aug 2018 15:30:00 +0300 8 | 9 | nginx-nr-agent (2.0.0-11) unstable; urgency=low 10 | 11 | * 2.0.0-11 12 | * avoid exiting on unhandled errors while fetching status 13 | 14 | -- Andrei Belov Tue, 18 Apr 2017 10:30:00 +0300 15 | 16 | nginx-nr-agent (2.0.0-10) unstable; urgency=low 17 | 18 | * 2.0.0-10 19 | * upstream keepalive connections metric reintroduced 20 | 21 | -- Andrei Belov Mon, 9 Jan 2017 15:50:00 +0300 22 | 23 | nginx-nr-agent (2.0.0-9) unstable; urgency=low 24 | 25 | * 2.0.0-9 26 | * lock permissions adjusted 27 | 28 | -- Andrei Belov Wed, 17 Aug 2016 14:30:00 +0300 29 | 30 | nginx-nr-agent (2.0.0-8) unstable; urgency=low 31 | 32 | * 2.0.0-8 33 | * fixed handling of caches configured without the "max_size" parameter 34 | * added support for HTTPS proxy 35 | 36 | -- Andrei Belov Mon, 1 Feb 2016 07:00:00 +0300 37 | 38 | nginx-nr-agent (2.0.0-7) unstable; urgency=low 39 | 40 | * 2.0.0-7 41 | * made compatible with nginx-plus-r7 (peer stats moved) 42 | 43 | -- Andrei Belov Mon, 21 Sep 2015 10:00:00 +0300 44 | 45 | nginx-nr-agent (2.0.0-6) unstable; urgency=low 46 | 47 | * 2.0.0-6 48 | * made compatible with nginx-plus-r6 (per-peer keepalive counter removed) 49 | 50 | -- Andrei Belov Mon, 6 Apr 2015 10:30:00 +0300 51 | 52 | nginx-nr-agent (2.0.0-5) unstable; urgency=low 53 | 54 | * 2.0.0-5 55 | * bundled documentation announced in post-install banner 56 | 57 | -- Andrei Belov Tue, 10 Mar 2015 15:00:00 +0300 58 | 59 | nginx-nr-agent (2.0.0-4) unstable; urgency=low 60 | 61 | * 2.0.0-4 62 | * fixed ZeroDivisionError while calculating cache hit ratios 63 | 64 | -- Andrei Belov Tue, 31 Oct 2014 14:50:00 -0800 65 | 66 | nginx-nr-agent (2.0.0-3) unstable; urgency=low 67 | 68 | * 2.0.0-3 69 | * fixed pidfile handling between reboots 70 | 71 | -- Andrei Belov Tue, 21 Oct 2014 11:20:00 -0800 72 | 73 | nginx-nr-agent (2.0.0-2) unstable; urgency=low 74 | 75 | * 2.0.0-2 76 | * fixed Content-Type header recognition 77 | 78 | -- Andrei Belov Fri, 17 Oct 2014 10:00:00 +0400 79 | 80 | nginx-nr-agent (2.0.0-1) unstable; urgency=low 81 | 82 | * 2.0.0 83 | * refactored from previous Ruby-based version to Python 84 | * provides more metrics for N+ (server zones, caches) 85 | 86 | -- Andrei Belov Wed, 10 Sep 2014 16:30:00 +0400 87 | -------------------------------------------------------------------------------- /rpm/SOURCES/nginx-nr-agent.init: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # nginx-nr-agent init script 3 | # 4 | # chkconfig: - 50 50 5 | # description: nginx New Relic agent daemon 6 | # 7 | # processname: /usr/bin/nginx-nr-agent.py 8 | # config: /etc/nginx-nr-agent/nginx-nr-agent.ini 9 | # pidfile: /var/run/nginx-nr-agent/nginx-nr-agent.pid 10 | 11 | ### BEGIN INIT INFO 12 | # Provides: nginx-nr-agent 13 | # Required-Start: $local_fs $network 14 | # Required-Stop: $local_fs $network 15 | # Should-Start: 16 | # Should-Stop: 17 | # Default-Start: 18 | # Default-Stop: 19 | # Short-Description: start and stop nginx-nr-agent daemon 20 | # Description: nginx New Relic agent daemon 21 | ### END INIT INFO 22 | 23 | # source function library 24 | . /etc/init.d/functions 25 | 26 | RETVAL=0 27 | prog="nginx-nr-agent" 28 | binary=/usr/bin/nginx-nr-agent.py 29 | pidfile=/var/run/nginx-nr-agent/nginx-nr-agent.pid 30 | 31 | [ -r /etc/sysconfig/nginx-nr-agent ] && . /etc/sysconfig/nginx-nr-agent 32 | 33 | start() { 34 | [ -x $binary ] || exit 5 35 | echo -n $"Starting $prog: " 36 | if [ $UID -ne 0 ]; then 37 | RETVAL=1 38 | failure 39 | else 40 | mkdir -p /var/run/nginx-nr-agent && chown nobody /var/run/nginx-nr-agent 41 | daemon --user=nobody $binary -p $pidfile start 42 | RETVAL=$? 43 | [ $RETVAL -eq 0 ] && touch /var/lock/subsys/nginx-nr-agent 44 | fi; 45 | echo 46 | return $RETVAL 47 | } 48 | 49 | stop() { 50 | echo -n $"Stopping $prog: " 51 | if [ $UID -ne 0 ]; then 52 | RETVAL=1 53 | failure 54 | else 55 | killproc -p $pidfile $binary 56 | RETVAL=$? 57 | [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/nginx-nr-agent 58 | fi; 59 | echo 60 | return $RETVAL 61 | } 62 | 63 | restart(){ 64 | stop 65 | start 66 | } 67 | 68 | condrestart(){ 69 | [ -e /var/lock/subsys/nginx-nr-agent ] && restart 70 | return 0 71 | } 72 | 73 | configtest(){ 74 | [ -x $binary ] || exit 5 75 | if [ $UID -ne 0 ]; then 76 | RETVAL=1 77 | failure 78 | else 79 | $binary -p $pidfile configtest 80 | RETVAL=$? 81 | fi; 82 | return $RETVAL 83 | } 84 | 85 | case "$1" in 86 | start) 87 | configtest || exit 1 88 | start 89 | RETVAL=$? 90 | ;; 91 | stop) 92 | stop 93 | RETVAL=$? 94 | ;; 95 | restart) 96 | configtest || exit 1 97 | restart 98 | RETVAL=$? 99 | ;; 100 | condrestart|try-restart) 101 | configtest || exit 1 102 | condrestart 103 | RETVAL=$? 104 | ;; 105 | status) 106 | status -p $pidfile nginx-nr-agent 107 | RETVAL=$? 108 | ;; 109 | configtest) 110 | configtest 111 | RETVAL=$? 112 | ;; 113 | *) 114 | echo $"Usage: $0 {start|stop|status|restart|condrestart|configtest}" 115 | RETVAL=2 116 | esac 117 | 118 | exit $RETVAL 119 | -------------------------------------------------------------------------------- /debian/init.d: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: nginx-nr-agent 4 | # Required-Start: $network $remote_fs $local_fs 5 | # Required-Stop: $network $remote_fs $local_fs 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: Stop/start nginx-nr-agent 9 | ### END INIT INFO 10 | 11 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 12 | DESC=nginx-nr-agent 13 | NAME=nginx-nr-agent 14 | CONFFILE=/etc/nginx-nr-agent/nginx-nr-agent.ini 15 | DAEMON=/usr/bin/nginx-nr-agent.py 16 | PIDFILE=/var/run/nginx-nr-agent/$NAME.pid 17 | SCRIPTNAME=/etc/init.d/$NAME 18 | 19 | [ -x $DAEMON ] || exit 0 20 | 21 | DAEMON_ARGS="-c $CONFFILE -p $PIDFILE" 22 | 23 | sysconfig=`/usr/bin/basename $SCRIPTNAME` 24 | 25 | . /lib/init/vars.sh 26 | 27 | . /lib/lsb/init-functions 28 | 29 | [ -r /etc/default/$sysconfig ] && . /etc/default/$sysconfig 30 | 31 | do_start() 32 | { 33 | mkdir -p /var/run/nginx-nr-agent && chown nobody /var/run/nginx-nr-agent 34 | start-stop-daemon --start --quiet --chuid nobody --exec $DAEMON -- $DAEMON_ARGS start 35 | RETVAL="$?" 36 | return "$RETVAL" 37 | } 38 | 39 | do_stop() 40 | { 41 | start-stop-daemon --stop --quiet --oknodo --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME 42 | RETVAL="$?" 43 | rm -f $PIDFILE 44 | return "$RETVAL" 45 | } 46 | 47 | do_configtest() { 48 | $DAEMON $DAEMON_ARGS configtest 49 | RETVAL="$?" 50 | return $RETVAL 51 | } 52 | 53 | case "$1" in 54 | start) 55 | do_configtest || exit 1 56 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC " "$NAME" 57 | do_start 58 | case "$?" in 59 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 60 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 61 | esac 62 | ;; 63 | stop) 64 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" 65 | do_stop 66 | case "$?" in 67 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 68 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 69 | esac 70 | ;; 71 | status) 72 | status_of_proc -p "$PIDFILE" "$DAEMON" "$NAME" && exit 0 || exit $? 73 | ;; 74 | configtest) 75 | do_configtest 76 | ;; 77 | restart|force-reload) 78 | log_daemon_msg "Restarting $DESC" "$NAME" 79 | do_configtest || exit 1 80 | do_stop 81 | case "$?" in 82 | 0|1) 83 | do_start 84 | case "$?" in 85 | 0) log_end_msg 0 ;; 86 | 1) log_end_msg 1 ;; # Old process is still running 87 | *) log_end_msg 1 ;; # Failed to start 88 | esac 89 | ;; 90 | *) 91 | # Failed to stop 92 | log_end_msg 1 93 | ;; 94 | esac 95 | ;; 96 | *) 97 | echo "Usage: $SCRIPTNAME {start|stop|status|restart|configtest}" >&2 98 | exit 3 99 | ;; 100 | esac 101 | 102 | exit $RETVAL 103 | -------------------------------------------------------------------------------- /rpm/SPECS/nginx-nr-agent.spec: -------------------------------------------------------------------------------- 1 | # 2 | Summary: New Relic agent for NGINX and NGINX Plus 3 | Name: nginx-nr-agent 4 | Version: 2.0.1 5 | Release: 1%{?dist}.ngx 6 | Vendor: Nginx Software, Inc. 7 | URL: https://www.nginx.com/ 8 | Packager: Nginx Software, Inc. 9 | 10 | Source0: nginx-nr-agent.py 11 | Source1: nginx-nr-agent.ini 12 | Source2: nginx-nr-agent.init 13 | Source3: COPYRIGHT 14 | Source4: nginx-nr-agent.logrotate 15 | Source5: nginx-nr-agent.sysconfig 16 | 17 | License: 2-clause BSD-like license 18 | Group: System Environment/Daemons 19 | 20 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root 21 | Requires: python >= 2.6 22 | Requires: python-daemon 23 | Requires: initscripts >= 8.36 24 | Requires(post): chkconfig 25 | 26 | BuildArch: noarch 27 | 28 | %description 29 | This package contains agent script used for collecting 30 | and reporting a number of metrics from NGINX and/or NGINX Plus 31 | instances to New Relic. 32 | 33 | %prep 34 | 35 | %build 36 | 37 | %install 38 | %{__rm} -rf $RPM_BUILD_ROOT 39 | 40 | %{__mkdir} -p $RPM_BUILD_ROOT%{_bindir} 41 | %{__install} -m 755 -p %{SOURCE0} $RPM_BUILD_ROOT%{_bindir}/ 42 | 43 | %{__mkdir} -p $RPM_BUILD_ROOT%{_sysconfdir}/nginx-nr-agent 44 | %{__install} -m 644 -p %{SOURCE1} $RPM_BUILD_ROOT%{_sysconfdir}/nginx-nr-agent/ 45 | 46 | %{__mkdir} -p $RPM_BUILD_ROOT%{_datadir}/doc/nginx-nr-agent 47 | %{__install} -m 644 -p %{SOURCE3} \ 48 | $RPM_BUILD_ROOT%{_datadir}/doc/nginx-nr-agent/ 49 | 50 | %{__mkdir} -p $RPM_BUILD_ROOT%{_initrddir} 51 | %{__install} -m755 %{SOURCE2} \ 52 | $RPM_BUILD_ROOT%{_initrddir}/nginx-nr-agent 53 | 54 | %{__mkdir} -p $RPM_BUILD_ROOT%{_sysconfdir}/logrotate.d 55 | %{__install} -m 644 -p %{SOURCE4} \ 56 | $RPM_BUILD_ROOT%{_sysconfdir}/logrotate.d/nginx-nr-agent 57 | 58 | %{__mkdir} -p $RPM_BUILD_ROOT%{_sysconfdir}/sysconfig 59 | %{__install} -m 755 -p %{SOURCE5} \ 60 | $RPM_BUILD_ROOT%{_sysconfdir}/sysconfig/nginx-nr-agent 61 | 62 | %clean 63 | %{__rm} -rf $RPM_BUILD_ROOT 64 | 65 | %files 66 | %defattr(-,root,root) 67 | %{_bindir}/nginx-nr-agent.py 68 | 69 | %{_initrddir}/nginx-nr-agent 70 | 71 | %dir %{_sysconfdir}/nginx-nr-agent 72 | %config(noreplace) %{_sysconfdir}/nginx-nr-agent/nginx-nr-agent.ini 73 | %config(noreplace) %{_sysconfdir}/logrotate.d/nginx-nr-agent 74 | %config(noreplace) %{_sysconfdir}/sysconfig/nginx-nr-agent 75 | 76 | %dir %{_datadir}/doc/nginx-nr-agent 77 | %{_datadir}/doc/nginx-nr-agent/* 78 | 79 | %post 80 | if [ $1 -eq 1 ]; then 81 | /sbin/chkconfig --add nginx-nr-agent 82 | mkdir -p /var/run/nginx-nr-agent 83 | touch /var/log/nginx-nr-agent.log 84 | chown nobody /var/run/nginx-nr-agent /var/log/nginx-nr-agent.log 85 | cat < /dev/null 2>&1 105 | /sbin/chkconfig --del nginx-nr-agent 106 | fi 107 | 108 | %changelog 109 | * Wed Aug 8 2018 Andrei Belov 110 | - 2.0.1_1 111 | - legacy status module support removed 112 | - new nginx-plus API support added 113 | 114 | * Tue Apr 18 2017 Andrei Belov 115 | - 2.0.0_12 116 | - avoid exiting on unhandled errors while fetching status 117 | 118 | * Mon Jan 9 2017 Andrei Belov 119 | - 2.0.0_11 120 | - upstream keepalive connections metric reintroduced 121 | 122 | * Wed Aug 17 2016 Andrei Belov 123 | - 2.0.0_10 124 | - lock permissions adjusted 125 | 126 | * Mon Feb 1 2016 Andrei Belov 127 | - 2.0.0_9 128 | - fixed handling of caches configured without the "max_size" parameter 129 | - added support for HTTPS proxy 130 | 131 | * Mon Sep 21 2015 Andrei Belov 132 | - 2.0.0_8 133 | - made compatible with nginx-plus-r7 (peer stats moved) 134 | 135 | * Mon Apr 6 2015 Andrei Belov 136 | - 2.0.0_7 137 | - made compatible with nginx-plus-r6 (per-peer keepalive counter removed) 138 | 139 | * Wed Mar 25 2015 Andrei Belov 140 | - 2.0.0_6 141 | - init script fixed for systems without setproctitle Python module 142 | 143 | * Tue Mar 10 2015 Andrei Belov 144 | - 2.0.0_5 145 | - bundled documentation announced in post-install banner 146 | 147 | * Tue Oct 31 2014 Andrei Belov 148 | - 2.0.0_4 149 | - fixed ZeroDivisionError while calculating cache hit ratios 150 | 151 | * Tue Oct 21 2014 Andrei Belov 152 | - 2.0.0_3 153 | - fixed pidfile handling between reboots 154 | 155 | * Fri Oct 17 2014 Andrei Belov 156 | - 2.0.0_2 157 | - fixed Content-Type header recognition 158 | 159 | * Wed Sep 10 2014 Andrei Belov 160 | - 2.0.0 161 | - refactored from previous Ruby-based version to Python 162 | - provides more metrics for N+ (server zones, caches) 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THIS REPO IS ARCHIVED 2 | 3 | # This repository has been archived. There will likely be no further development on the project and security vulnerabilities may be unaddressed. 4 | 5 | NGINX plugin for New Relic 6 | 7 | ## Preface 8 | 9 | Visualize performance metrics in New Relic for the open source NGINX software and NGINX Plus. 10 | 11 | It allows to collect and report various important counters from an NGINX instance such as: 12 | 13 | * Active client connections 14 | * Idle (keepalive) client connections 15 | * Client connections accept rate, drop rate 16 | * Request rate 17 | 18 | NGINX Plus customers will be able to use additional set of metrics 19 | related to upstream monitoring (a number of servers, breakdown by state; 20 | upstream servers connections; bandwidth usage; backend response rate, 21 | breakdown by HTTP status code; healthchecks status), virtual servers 22 | summary stats (requests/responses rate, bandwidth usage), cache zone 23 | stats (responses by cache status, traffic from cache). 24 | 25 | Metrics will be charted in the New Relic User Interface and you will be able 26 | to configure alerts based on the values reported by this plugin. 27 | 28 | ## Requirements 29 | 30 | In order to use this plugin, you must have an active New Relic account. 31 | 32 | Plugin should work on any generic Unix environment with the following 33 | software components installed: 34 | 35 | * Python (2.6, 2.7) 36 | * python-daemon 37 | * make 38 | * **For CentOS/RHEL only:** initscripts 39 | * python-setproctitle (optional) 40 | 41 | ### Requirement for RHEL/CentOS build 42 | 43 | * rpm-build 44 | 45 | ### Requirements for Debian/Ubuntu build 46 | 47 | * dpkg-dev 48 | * debhelper 49 | 50 | ## Build 51 | 52 | You can build this tool for rpm or debian using the Makefile. Output will be in the `build_output/` directory. 53 | 54 | 55 | ```console 56 | $ make rpm 57 | ``` 58 | 59 | ```console 60 | $ make debian 61 | ``` 62 | 63 | ## Configuration 64 | 65 | ### NGINX 66 | 67 | To configure this plugin to work with the open source NGINX software using ngx_http_stub_status 68 | module (http://nginx.org/en/docs/http/ngx_http_stub_status_module.html), 69 | you have to special location containing "stub_status" directive, e.g.: 70 | 71 | Example #1: listen on localhost, access from 127.0.0.1 only: 72 | 73 | ```nginx 74 | server { 75 | listen 127.0.0.1:80; 76 | server_name localhost; 77 | 78 | location = /nginx_stub_status { 79 | stub_status on; 80 | allow 127.0.0.1; 81 | deny all; 82 | } 83 | } 84 | ``` 85 | 86 | Example #2: listen on `*:80`, access limited by HTTP basic auth: 87 | 88 | ```nginx 89 | server { 90 | listen 80; 91 | server_name example.com; 92 | 93 | location = /nginx_stub_status { 94 | stub_status on; 95 | auth_basic "nginx status"; 96 | auth_basic_user_file /path/to/auth_file; 97 | } 98 | } 99 | ``` 100 | 101 | Please follow this link to get more information about HTTP basic auth: 102 | http://nginx.org/en/docs/http/ngx_http_auth_basic_module.html 103 | 104 | 105 | ### NGINX Plus 106 | 107 | To configure this plugin to work with NGINX Plus using api 108 | module (http://nginx.org/en/docs/http/ngx_http_api_module.html), 109 | you have to add special location containing "api" directive, e.g.: 110 | 111 | Example #1: for NGINX Plus status, listen on `*:80`, authorized access: 112 | 113 | ```nginx 114 | server { 115 | listen 80; 116 | server_name example.com; 117 | 118 | location /api { 119 | api; 120 | auth_basic "nginx api"; 121 | auth_basic_user_file /path/to/auth_file; 122 | } 123 | } 124 | ``` 125 | 126 | (see http://nginx.org/en/docs/http/ngx_http_api_module.html#api for details) 127 | 128 | Do not forget to reload nginx after changing the configuration. 129 | 130 | 131 | ### Plugin configuration 132 | 133 | Edit nginx-nr-agent.ini configuration file: 134 | 135 | * insert your New Relic license key. 136 | 137 | * configure data sources (your nginx instances) using the following parameters: 138 | * url (required): full URL pointing to stub_status (open source NGINX software) or api (NGINX Plus) output. 139 | * name (required): name of the instance as it will be shown in the New Relic UI. 140 | * http_user, http_pass (optional): credentials used for 141 | HTTP basic authorization. 142 | 143 | 144 | ### Configuring HTTPS proxy to access New Relic API endpoint 145 | 146 | In case when a host with nginx-nr-agent plugin is behind a proxy, 147 | there is an ability to add proxy URL to the startup configuration script: 148 | * /etc/sysconfig/nginx-nr-agent in RHEL/CentOS. 149 | * /etc/default/nginx-nr-agent in Debian/Ubuntu. 150 | 151 | It should be done in a form of exporting the HTTPS_PROXY variable, e.g.: 152 | export HTTPS_PROXY="your-proxy-host.example.com:3128" 153 | 154 | 155 | ## Running the plugin 156 | 157 | Plugin can be started as a daemon (default) or in foreground mode. 158 | In order to start it daemonized, use the following command under root: 159 | 160 | ```console 161 | $ service nginx-nr-agent start 162 | ``` 163 | 164 | By default, plugin is running under "nobody" user, writing log 165 | into /var/log/nginx-nr-agent.log. 166 | 167 | Plugin status can be checked by running: 168 | 169 | ```console 170 | $ service nginx-nr-agent status 171 | ``` 172 | 173 | To stop plugin, use: 174 | 175 | ```console 176 | $ service nginx-nr-agent stop 177 | ``` 178 | 179 | For debugging purposes, you can launch the plugin in foreground mode, 180 | with all output going to stdout: 181 | 182 | ```console 183 | $ nginx-nr-agent.py -f start 184 | ``` 185 | 186 | Carefully check plugin's output for any possible error messages. 187 | In case of success, collected data should appear in the New Relic 188 | user interface shortly after starting. 189 | 190 | ## Support 191 | This tool will work with NGINX version R13 and above but support and maintenance of this project will stop at R16 192 | -------------------------------------------------------------------------------- /nginx-nr-agent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (C) Andrei Belov 4 | # Copyright (C) Nginx, Inc. 5 | # 6 | 7 | import logging, logging.config 8 | import os, re, sys, ConfigParser 9 | import json 10 | import base64 11 | from time import sleep, time 12 | from urllib2 import Request, urlopen, URLError, HTTPError 13 | from daemon import runner 14 | import traceback 15 | 16 | NEWRELIC_API_URL = 'https://platform-api.newrelic.com/platform/v1/metrics' 17 | AGENT_GUID = 'com.nginx.newrelic-agent' 18 | AGENT_VERSION = '2.0.1' 19 | API_VERSION = '1' 20 | 21 | DEFAULT_CONFIG_FILE = '/etc/nginx-nr-agent/nginx-nr-agent.ini' 22 | DEFAULT_PID_FILE = '/var/run/nginx-nr-agent/nginx-nr-agent.pid' 23 | DEFAULT_POLL_INTERVAL = 60.0 24 | 25 | LOG = logging.getLogger('nginx-nr-agent') 26 | 27 | class NginxApiCollector(object): 28 | 29 | def __init__(self, section, name, url, poll_interval): 30 | self.section = section 31 | self.name = name 32 | self.url = url.rstrip('/') 33 | self.basic_auth = None 34 | self.gauges = dict() 35 | self.derives = dict() 36 | self.deltas = dict() 37 | self.prevupdate = 0.0 38 | self.lastupdate = 0.0 39 | self.unpushed = [] 40 | self.poll_interval = poll_interval 41 | 42 | def update_gauge(self, metric, units, value): 43 | self.gauges[metric] = value 44 | self.unpushed.append({ 'metric': metric, 'value': value, 'units': units, 'timestamp': self.lastupdate }) 45 | LOG.debug("update gauge %s: rv=%.2f", metric, value) 46 | 47 | def update_derive(self, metric, units, value): 48 | if metric not in self.derives.keys(): 49 | self.derives[metric] = value 50 | return 51 | 52 | delta = value - self.derives[metric] 53 | if delta < 0: 54 | LOG.info("derive counter for %s was reset, skipping update", metric) 55 | self.derives[metric] = value 56 | return 57 | 58 | timedelta = float(time() - self.prevupdate) if self.prevupdate else float(self.poll_interval) 59 | rv = float(delta / timedelta) 60 | self.unpushed.append({ 'metric': metric, 'value': rv, 'units': units, 'timestamp': self.lastupdate }) 61 | LOG.debug("update derive %s: pv=%d cv=%d rv=%.3f td=%.3f", metric, 62 | self.derives[metric], value, rv, timedelta) 63 | self.derives[metric] = value 64 | self.deltas[metric] = delta 65 | 66 | def get_api_json(self, uri): 67 | resp = self.get_request(uri) 68 | if resp == None: 69 | return resp 70 | try: 71 | js = json.loads(resp.read()) 72 | except ValueError, e: 73 | LOG.error("could not parse JSON from new api body: '%s'", resp.read()) 74 | return None 75 | return js 76 | 77 | def get_base_type(self): 78 | resp = self.get_request("") 79 | if resp == None: 80 | return resp 81 | return resp.info().getheader('Content-Type') 82 | 83 | def get_request(self, uri): 84 | r = Request(self.url + uri) 85 | if self.basic_auth: 86 | r.add_header('Authorization', "Basic %s" % self.basic_auth) 87 | try: 88 | u = urlopen(r) 89 | except HTTPError as e: 90 | LOG.error("request for %s returned %d", self.url + uri, e.code) 91 | return None 92 | except URLError as e: 93 | LOG.error("request for %s failed: %s", self.url + uri, e.reason) 94 | return None 95 | except: 96 | LOG.error("EXCEPTION while fetching api: %s", traceback.format_exc()) 97 | return None 98 | return u 99 | 100 | 101 | def update_base_stats(self, stats): 102 | # conn/accepted, conn/dropped, conn/active, conn/idle, reqs/total, reqs/current 103 | self.update_derive('conn/accepted', 'Connections/sec', stats[0]) 104 | self.update_derive('conn/dropped', 'Connections/sec', stats[1]) 105 | self.update_gauge('conn/active', 'Connections', stats[2]) 106 | self.update_gauge('conn/idle', 'Connections', stats[3]) 107 | self.update_derive('reqs/total', 'Requests/sec', stats[4]) 108 | self.update_gauge('reqs/current', 'Requests', stats[5]) 109 | 110 | def update_extended_stats(self,): 111 | LOG.debug("updating extended metrics for api version %s", API_VERSION) 112 | 113 | upstreams = self.get_api_json("/http/upstreams") 114 | if upstreams: 115 | LOG.debug("collecting extra metrics for %d upstreams", len(upstreams)) 116 | 117 | u_srv_up, u_srv_down, u_srv_unavail, u_srv_unhealthy = 0, 0, 0, 0 118 | u_conn_active, u_conn_keepalive = 0, 0 119 | u_reqs, u_resp, u_resp_1xx, u_resp_2xx, u_resp_3xx, u_resp_4xx, u_resp_5xx = 0, 0, 0, 0, 0, 0, 0 120 | u_sent, u_received = 0, 0 121 | u_fails, u_unavail = 0, 0 122 | u_hc_checks, u_hc_fails, u_hc_unhealthy = 0, 0, 0 123 | 124 | for u in upstreams.itervalues(): 125 | u_conn_keepalive += u['keepalive'] 126 | upeers = u['peers'] 127 | for us in upeers: 128 | if us['state'] == 'up': 129 | u_srv_up += 1 130 | elif us['state'] == 'down': 131 | u_srv_down += 1 132 | elif us['state'] == 'unavail': 133 | u_srv_unavail += 1 134 | elif us['state'] == 'unhealthy': 135 | u_srv_unhealthy += 1 136 | 137 | u_conn_active += us['active'] 138 | u_reqs += us['requests'] 139 | u_resp += us['responses']['total'] 140 | u_resp_1xx += us['responses']['1xx'] 141 | u_resp_2xx += us['responses']['2xx'] 142 | u_resp_3xx += us['responses']['3xx'] 143 | u_resp_4xx += us['responses']['4xx'] 144 | u_resp_5xx += us['responses']['5xx'] 145 | u_sent += us['sent'] 146 | u_received += us['received'] 147 | u_fails += us['fails'] 148 | u_unavail += us['unavail'] 149 | u_hc_checks += us['health_checks']['checks'] 150 | u_hc_fails += us['health_checks']['fails'] 151 | u_hc_unhealthy += us['health_checks']['unhealthy'] 152 | 153 | self.update_gauge('upstream/servers/up', 'Servers', u_srv_up) 154 | self.update_gauge('upstream/servers/down', 'Servers', u_srv_down) 155 | self.update_gauge('upstream/servers/unavail', 'Servers', u_srv_unavail) 156 | self.update_gauge('upstream/servers/unhealthy', 'Servers', u_srv_unhealthy) 157 | 158 | self.update_gauge('upstream/conn/active', 'Connections', u_conn_active) 159 | self.update_gauge('upstream/conn/keepalive', 'Connections', u_conn_keepalive) 160 | 161 | self.update_derive('upstream/reqs', 'Requests/sec', u_reqs) 162 | self.update_derive('upstream/resp', 'Responses/sec', u_resp) 163 | self.update_derive('upstream/resp/1xx', 'Responses/sec', u_resp_1xx) 164 | self.update_derive('upstream/resp/2xx', 'Responses/sec', u_resp_2xx) 165 | self.update_derive('upstream/resp/3xx', 'Responses/sec', u_resp_3xx) 166 | self.update_derive('upstream/resp/4xx', 'Responses/sec', u_resp_4xx) 167 | self.update_derive('upstream/resp/5xx', 'Responses/sec', u_resp_5xx) 168 | 169 | self.update_derive('upstream/traffic/sent', 'Bytes/sec', u_sent) 170 | self.update_derive('upstream/traffic/received', 'Bytes/sec', u_received) 171 | 172 | self.update_gauge('upstream/server/fails', 'times', u_fails) 173 | self.update_gauge('upstream/server/unavails', 'times', u_unavail) 174 | self.update_gauge('upstream/hc/total', 'times', u_hc_checks) 175 | self.update_gauge('upstream/hc/fails', 'times', u_hc_fails) 176 | self.update_gauge('upstream/hc/unhealthies', 'times', u_hc_unhealthy) 177 | 178 | server_zones = self.get_api_json("/http/server_zones") 179 | if server_zones: 180 | LOG.debug("collecting extra metrics for %d server_zones", len(server_zones)) 181 | 182 | sz_processing, sz_requests, sz_received, sz_sent = 0, 0, 0, 0 183 | sz_resp, sz_resp_1xx, sz_resp_2xx, sz_resp_3xx, sz_resp_4xx, sz_resp_5xx = 0, 0, 0, 0, 0, 0 184 | 185 | for sz in server_zones.itervalues(): 186 | sz_processing += sz['processing'] 187 | sz_requests += sz['requests'] 188 | sz_received += sz['received'] 189 | sz_sent += sz['sent'] 190 | sz_resp += sz['responses']['total'] 191 | sz_resp_1xx += sz['responses']['1xx'] 192 | sz_resp_2xx += sz['responses']['2xx'] 193 | sz_resp_3xx += sz['responses']['3xx'] 194 | sz_resp_4xx += sz['responses']['4xx'] 195 | sz_resp_5xx += sz['responses']['5xx'] 196 | 197 | self.update_gauge('sz/processing', 'Requests', sz_processing) 198 | self.update_derive('sz/requests', 'Requests/sec', sz_requests) 199 | self.update_derive('sz/received', 'Bytes/sec', sz_received) 200 | self.update_derive('sz/sent', 'Bytes/sec', sz_sent) 201 | self.update_derive('sz/resp', 'Responses/sec', sz_resp) 202 | self.update_derive('sz/resp/1xx', 'Responses/sec', sz_resp_1xx) 203 | self.update_derive('sz/resp/2xx', 'Responses/sec', sz_resp_2xx) 204 | self.update_derive('sz/resp/3xx', 'Responses/sec', sz_resp_3xx) 205 | self.update_derive('sz/resp/4xx', 'Responses/sec', sz_resp_4xx) 206 | self.update_derive('sz/resp/5xx', 'Responses/sec', sz_resp_5xx) 207 | 208 | caches = self.get_api_json("/http/caches") 209 | if caches: 210 | LOG.debug("collecting extra metrics for %d caches", len(caches)) 211 | 212 | cache_size, cache_max_size = 0, 0 213 | cache_resp_hit, cache_resp_stale, cache_resp_updating, cache_resp_revalidated = 0, 0, 0, 0 214 | cache_bytes_hit, cache_bytes_stale, cache_bytes_updating, cache_bytes_revalidated = 0, 0, 0, 0 215 | cache_resp_miss, cache_resp_expired, cache_resp_bypass = 0, 0, 0 216 | cache_bytes_miss, cache_bytes_expired, cache_bytes_bypass = 0, 0, 0 217 | cache_resp_written_miss, cache_resp_written_expired, cache_resp_written_bypass = 0, 0, 0 218 | cache_bytes_written_miss, cache_bytes_written_expired, cache_bytes_written_bypass = 0, 0, 0 219 | 220 | for c in caches.itervalues(): 221 | cache_size += c['size'] 222 | cache_max_size += c.get('max_size', 0) 223 | cache_resp_hit += c['hit']['responses'] 224 | cache_resp_stale += c['stale']['responses'] 225 | cache_resp_updating += c['updating']['responses'] 226 | cache_resp_revalidated += c['revalidated']['responses'] 227 | cache_bytes_hit += c['hit']['bytes'] 228 | cache_bytes_stale += c['stale']['bytes'] 229 | cache_bytes_updating += c['updating']['bytes'] 230 | cache_bytes_revalidated += c['revalidated']['bytes'] 231 | cache_resp_miss += c['miss']['responses'] 232 | cache_resp_expired += c['expired']['responses'] 233 | cache_resp_bypass += c['bypass']['responses'] 234 | cache_bytes_miss += c['miss']['bytes'] 235 | cache_bytes_expired += c['expired']['bytes'] 236 | cache_bytes_bypass += c['bypass']['bytes'] 237 | cache_resp_written_miss += c['miss']['responses_written'] 238 | cache_resp_written_expired += c['expired']['responses_written'] 239 | cache_resp_written_bypass += c['bypass']['responses_written'] 240 | cache_bytes_written_miss += c['miss']['bytes_written'] 241 | cache_bytes_written_expired += c['expired']['bytes_written'] 242 | cache_bytes_written_bypass += c['bypass']['bytes_written'] 243 | 244 | self.update_gauge('cache/size', 'Bytes', cache_size) 245 | self.update_gauge('cache/max_size', 'Bytes', cache_max_size) 246 | self.update_derive('cache/resp/hit', 'Responses/sec', cache_resp_hit) 247 | self.update_derive('cache/resp/stale', 'Responses/sec', cache_resp_stale) 248 | self.update_derive('cache/resp/updating', 'Responses/sec', cache_resp_updating) 249 | self.update_derive('cache/resp/revalidated', 'Responses/sec', cache_resp_revalidated) 250 | self.update_derive('cache/bytes/hit', 'Bytes/sec', cache_bytes_hit) 251 | self.update_derive('cache/bytes/stale', 'Bytes/sec', cache_bytes_stale) 252 | self.update_derive('cache/bytes/updating', 'Bytes/sec', cache_bytes_updating) 253 | self.update_derive('cache/bytes/revalidated', 'Bytes/sec', cache_bytes_revalidated) 254 | self.update_derive('cache/resp/miss', 'Responses/sec', cache_resp_miss) 255 | self.update_derive('cache/resp/expired', 'Responses/sec', cache_resp_expired) 256 | self.update_derive('cache/resp/bypass', 'Responses/sec', cache_resp_bypass) 257 | self.update_derive('cache/bytes/miss', 'Bytes/sec', cache_bytes_miss) 258 | self.update_derive('cache/bytes/expired', 'Bytes/sec', cache_bytes_expired) 259 | self.update_derive('cache/bytes/bypass', 'Bytes/sec', cache_bytes_bypass) 260 | self.update_derive('cache/resp_written/miss', 'Responses/sec', cache_resp_written_miss) 261 | self.update_derive('cache/resp_written/expired', 'Responses/sec', cache_resp_written_expired) 262 | self.update_derive('cache/resp_written/bypass', 'Responses/sec', cache_resp_written_bypass) 263 | self.update_derive('cache/bytes_written/miss', 'Bytes/sec', cache_bytes_written_miss) 264 | self.update_derive('cache/bytes_written/expired', 'Bytes/sec', cache_bytes_written_expired) 265 | self.update_derive('cache/bytes_written/bypass', 'Bytes/sec', cache_bytes_written_bypass) 266 | 267 | cache_resp_cached = cache_resp_hit + cache_resp_stale + cache_resp_updating + cache_resp_revalidated 268 | cache_resp_uncached = cache_resp_miss + cache_resp_expired + cache_resp_bypass 269 | if (cache_resp_cached + cache_resp_uncached) > 0: 270 | cache_hit_ratio_long = (cache_resp_cached / float(cache_resp_cached + cache_resp_uncached)) * 100.0 271 | self.update_gauge('cache/hitratio/long', 'Percent', cache_hit_ratio_long) 272 | 273 | if 'cache/resp/hit' in self.deltas.keys(): 274 | cache_resp_cached = (self.deltas['cache/resp/hit'] + self.deltas['cache/resp/stale'] + 275 | self.deltas['cache/resp/updating'] + self.deltas['cache/resp/revalidated']) 276 | cache_resp_uncached = (self.deltas['cache/resp/miss'] + self.deltas['cache/resp/expired'] + 277 | self.deltas['cache/resp/bypass']) 278 | if (cache_resp_cached + cache_resp_uncached) > 0: 279 | cache_hit_ratio_short = (cache_resp_cached / float(cache_resp_cached + cache_resp_uncached)) * 100.0 280 | self.update_gauge('cache/hitratio/short', 'Percent', cache_hit_ratio_short) 281 | 282 | def process_stub_status(self): 283 | body = self.get_request("").read() 284 | if body is None: 285 | LOG.error("stub_status returned nothing to process") 286 | return False 287 | LOG.debug("processing stub status for %s", self.name) 288 | STUB_RE = re.compile(r'^Active connections: (?P\d+)\s+[\w ]+\n' 289 | r'\s+(?P\d+)' 290 | r'\s+(?P\d+)' 291 | r'\s+(?P\d+)' 292 | r'\s+Reading:\s+(?P\d+)' 293 | r'\s+Writing:\s+(?P\d+)' 294 | r'\s+Waiting:\s+(?P\d+)') 295 | m = STUB_RE.match(body) 296 | if not m: 297 | LOG.error("could not parse stub status body (len=%d): '%s'", len(body), body) 298 | return False 299 | self.lastupdate = time() 300 | self.update_base_stats([ 301 | int(m.group('accepts')), 302 | int(m.group('accepts')) - int(m.group('handled')), 303 | int(m.group('connections')), 304 | int(m.group('waiting')), 305 | int(m.group('requests')), 306 | int(m.group('reading')) + int(m.group('writing'))]) 307 | return True 308 | 309 | def process_new_api(self): 310 | LOG.debug("processing new api for %s", self.name) 311 | if self.url[-1] != API_VERSION: 312 | self.url += '/' + API_VERSION 313 | connections = self.get_api_json("/connections") 314 | requests = self.get_api_json("/http/requests") 315 | if connections is None or requests is None: 316 | return False 317 | self.lastupdate = time() 318 | self.update_base_stats([ 319 | connections['accepted'], 320 | connections['dropped'], 321 | connections['active'], 322 | connections['idle'], 323 | requests['total'], 324 | requests['current']]) 325 | self.update_extended_stats() 326 | return True 327 | 328 | def poll(self): 329 | LOG.debug("getting data from %s (lastupdate=%.3f)", self.url, self.lastupdate) 330 | ct = self.get_base_type() 331 | if ct.startswith('text/plain'): 332 | rc = self.process_stub_status() 333 | elif ct.startswith('application/json'): 334 | rc = self.process_new_api() 335 | else: 336 | LOG.error("unknown Content-Type from %s: '%s'", self.url, ct) 337 | return False 338 | self.prevupdate = self.lastupdate 339 | return rc 340 | 341 | class NginxNewRelicAgent(): 342 | 343 | def __init__(self): 344 | self.stdin_path = '/dev/null' 345 | self.stdout_path = '/dev/null' 346 | self.stderr_path = '/dev/null' 347 | self.pidfile_path = DEFAULT_PID_FILE 348 | self.pidfile_timeout = 5 349 | self.config = None 350 | self.config_file = DEFAULT_CONFIG_FILE 351 | self.foreground = False 352 | self.poll_interval = DEFAULT_POLL_INTERVAL 353 | self.license_key = None 354 | self.sources = [] 355 | self.metric_names = { 356 | 'conn/accepted': [ 'Connections/Accepted' ], 357 | 'conn/dropped': [ 'Connections/Dropped' ], 358 | 'conn/active': [ 'Connections/Active', 'ConnSummary/Active' ], 359 | 'conn/idle': [ 'Connections/Idle', 'ConnSummary/Idle' ], 360 | 'reqs/total': [ 'Requests/Total' ], 361 | 'reqs/current': [ 'Requests/Current' ], 362 | 'upstream/servers/up': [ 'UpstreamServers/Up' ], 363 | 'upstream/servers/down': [ 'UpstreamServers/Down' ], 364 | 'upstream/servers/unavail': [ 'UpstreamServers/Unavailable' ], 365 | 'upstream/servers/unhealthy': [ 'UpstreamServers/Unhealthy' ], 366 | 'upstream/conn/active': [ 'UpstreamConnections/Active' ], 367 | 'upstream/conn/keepalive': [ 'UpstreamConnections/Keepalive' ], 368 | 'upstream/reqs': [ 'UpstreamReqsResp/Requests' ], 369 | 'upstream/resp': [ 'UpstreamReqsResp/Responses' ], 370 | 'upstream/resp/1xx': [ 'UpstreamResponses/1xx' ], 371 | 'upstream/resp/2xx': [ 'UpstreamResponses/2xx' ], 372 | 'upstream/resp/3xx': [ 'UpstreamResponses/3xx' ], 373 | 'upstream/resp/4xx': [ 'UpstreamResponses/4xx' ], 374 | 'upstream/resp/5xx': [ 'UpstreamResponses/5xx' ], 375 | 'upstream/traffic/sent': [ 'UpstreamTraffic/Sent' ], 376 | 'upstream/traffic/received': [ 'UpstreamTraffic/Received' ], 377 | 'upstream/server/fails': [ 'UpstreamMisc/ServerFails' ], 378 | 'upstream/server/unavails': [ 'UpstreamMisc/ServerUnavailable' ], 379 | 'upstream/hc/total': [ 'UpstreamMisc/HealthChecksTotal' ], 380 | 'upstream/hc/fails': [ 'UpstreamMisc/HealthChecksFails' ], 381 | 'upstream/hc/unhealthies': [ 'UpstreamMisc/HealthChecksUnhealthy' ], 382 | 'sz/processing': [ 'ServerZone/Processing' ], 383 | 'sz/requests': [ 'ServerZoneReqsResp/Requests' ], 384 | 'sz/resp': [ 'ServerZoneReqsResp/Responses' ], 385 | 'sz/resp/1xx': [ 'ServerZoneResponses/1xx' ], 386 | 'sz/resp/2xx': [ 'ServerZoneResponses/2xx' ], 387 | 'sz/resp/3xx': [ 'ServerZoneResponses/3xx' ], 388 | 'sz/resp/4xx': [ 'ServerZoneResponses/4xx' ], 389 | 'sz/resp/5xx': [ 'ServerZoneResponses/5xx' ], 390 | 'sz/sent': [ 'ServerZoneTraffic/Sent' ], 391 | 'sz/received': [ 'ServerZoneTraffic/Received' ], 392 | 'cache/hitratio/long': [ 'CacheHitRatio/Long' ], 393 | 'cache/hitratio/short': [ 'CacheHitRatio/Short' ], 394 | 'cache/size': [ 'CacheSize/Size' ], 395 | 'cache/max_size': [ 'CacheSize/MaxSize' ], 396 | 'cache/resp/hit': [ 'CachedResponses/Hit' ], 397 | 'cache/resp/stale': [ 'CachedResponses/Stale' ], 398 | 'cache/resp/updating': [ 'CachedResponses/Updating' ], 399 | 'cache/resp/revalidated': [ 'CachedResponses/Revalidated' ], 400 | 'cache/bytes/hit': [ 'CachedBytes/Hit' ], 401 | 'cache/bytes/stale': [ 'CachedBytes/Stale' ], 402 | 'cache/bytes/updating': [ 'CachedBytes/Updating' ], 403 | 'cache/bytes/revalidated': [ 'CachedBytes/Revalidated' ], 404 | 'cache/resp/miss': [ 'UncachedResponses/Miss' ], 405 | 'cache/resp/expired': [ 'UncachedResponses/Expired' ], 406 | 'cache/resp/bypass': [ 'UncachedResponses/Bypass' ], 407 | 'cache/bytes/miss': [ 'UncachedBytes/Miss' ], 408 | 'cache/bytes/expired': [ 'UncachedBytes/Expired' ], 409 | 'cache/bytes/bypass': [ 'UncachedBytes/Bypass' ], 410 | 'cache/resp_written/miss': [ 'UncachedResponsesWritten/Miss' ], 411 | 'cache/resp_written/expired': [ 'UncachedResponsesWritten/Expired' ], 412 | 'cache/resp_written/bypass': [ 'UncachedResponsesWritten/Bypass' ], 413 | 'cache/bytes_written/miss': [ 'UncachedBytesWritten/Miss' ], 414 | 'cache/bytes_written/expired': [ 'UncachedBytesWritten/Expired' ], 415 | 'cache/bytes_written/bypass': [ 'UncachedBytesWritten/Bypass' ] 416 | } 417 | 418 | def newrelic_push(self): 419 | components = [] 420 | metrics_total = 0 421 | for ns in self.sources: 422 | if len(ns.unpushed) == 0: 423 | continue 424 | LOG.debug("composing push data for %s (%d entries)", ns.name, len(ns.unpushed)) 425 | component = dict() 426 | metrics = dict() 427 | component['guid'] = AGENT_GUID 428 | component['duration'] = self.poll_interval 429 | component['name'] = ns.name 430 | for m in ns.unpushed: 431 | for mn in self.metric_names[m['metric']]: 432 | metrics["Component/%s[%s]" % (mn, m['units'])] = m['value'] 433 | metrics_total += 1 434 | component['metrics'] = metrics 435 | components.append(component) 436 | del ns.unpushed[:] 437 | 438 | if len(components) == 0: 439 | return 440 | 441 | LOG.info("pushing %d metrics for %d components", metrics_total, len(components)) 442 | 443 | payload = dict() 444 | payload['agent'] = { 'version': AGENT_VERSION } 445 | payload['components'] = components 446 | 447 | LOG.debug("JSON payload: '%s'", json.dumps(payload)) 448 | 449 | r = Request(NEWRELIC_API_URL) 450 | r.add_header('Content-Type', 'application/json') 451 | r.add_header('Accept', 'application/json') 452 | r.add_header('User-Agent', "newrelic-nginx-agent/%s" % AGENT_VERSION) 453 | r.add_header('X-License-Key', self.license_key) 454 | 455 | try: 456 | u = urlopen(r, data=json.dumps(payload)) 457 | except HTTPError as e: 458 | LOG.error("POST request for %s returned %d", NEWRELIC_API_URL, e.code) 459 | LOG.debug("response:\n%s\n%s", e.headers, e.read()) 460 | return 461 | except URLError as e: 462 | LOG.error("POST request for %s failed: %s", NEWRELIC_API_URL, e.reason) 463 | return 464 | except: 465 | LOG.error("EXCEPTION while pushing metrics: %s", traceback.format_exc()) 466 | return 467 | 468 | response_body = u.read() 469 | LOG.debug("response:\n%s\n%s", u.headers, response_body) 470 | 471 | try: 472 | js = json.loads(response_body) 473 | except ValueError, e: 474 | LOG.error("could not parse JSON from response body: '%s'", response_body) 475 | return False 476 | 477 | if js['status'] != 'ok': 478 | LOG.error("push failed with status response: %s", js['status']) 479 | return False 480 | 481 | LOG.info("pushing finished successfully") 482 | return True 483 | 484 | def read_config(self): 485 | if self.config: 486 | return 487 | 488 | config = ConfigParser.RawConfigParser() 489 | config.read(self.config_file) 490 | 491 | for s in config.sections(): 492 | if s == 'global': 493 | if config.has_option(s, 'poll_interval'): 494 | self.poll_interval = int(config.get(s, 'poll_interval')) 495 | if config.has_option(s, 'newrelic_license_key'): 496 | self.license_key = config.get(s, 'newrelic_license_key') 497 | continue 498 | if not config.has_option(s, 'name') or not config.has_option(s, 'url'): 499 | continue 500 | ns = NginxApiCollector(s, config.get(s, 'name'), config.get(s, 'url'), self.poll_interval) 501 | if config.has_option(s, 'http_user') and config.has_option(s, 'http_pass'): 502 | ns.basic_auth = base64.b64encode(config.get(s, 'http_user') + b':' + config.get(s, 'http_pass')) 503 | self.sources.append(ns) 504 | self.config = config 505 | 506 | def configtest(self): 507 | self.read_config() 508 | 509 | if not self.license_key: 510 | LOG.error("no license key defined") 511 | sys.exit(1) 512 | 513 | if len(self.sources) == 0: 514 | LOG.error("no data sources configured - nothing to do") 515 | sys.exit(1) 516 | 517 | def run(self): 518 | LOG.info('using configuration from %s', self.config_file) 519 | self.configtest() 520 | 521 | LOG.info('starting with %d configured data sources, poll_interval=%d', 522 | len(self.sources), self.poll_interval) 523 | 524 | while True: 525 | try: 526 | for ns in self.sources: 527 | LOG.info("polling %s", ns.section) 528 | if ns.poll(): 529 | LOG.info("polling %s finished successfully", ns.section) 530 | else: 531 | LOG.error("polling %s failed", ns.section) 532 | self.newrelic_push() 533 | sleep(self.poll_interval) 534 | except KeyboardInterrupt: 535 | LOG.info("exiting due to KeyboardInterrupt") 536 | sys.exit(0) 537 | except SystemExit: 538 | LOG.info("exiting") 539 | sys.exit(0) 540 | 541 | class MyDaemonRunner(runner.DaemonRunner): 542 | 543 | def __init__(self, app): 544 | self._app = app 545 | self.detach_process = True 546 | runner.DaemonRunner.__init__(self, app) 547 | self.daemon_context.umask = 0022 548 | self.action_funcs['configtest'] = MyDaemonRunner._configtest 549 | 550 | def _configtest(self): 551 | self._app.configtest() 552 | 553 | def show_usage(self, rc): 554 | print "usage: %s [options] action" % sys.argv[0] 555 | print "valid actions: start, stop, configtest" 556 | print " -c, --config path to configuration file" 557 | print " -p, --pidfile path to pidfile" 558 | print " -f, --foreground do not detach from terminal" 559 | sys.exit(rc) 560 | 561 | def parse_args(self, argv=None): 562 | import getopt 563 | 564 | if len(sys.argv) < 2: 565 | self.show_usage(0) 566 | 567 | try: 568 | opts, args = getopt.getopt(sys.argv[1:], 'c:p:f', 569 | ['config=', 'pidfile=', 'foreground']) 570 | except getopt.GetoptError as e: 571 | print "Error: %s" % str(e) 572 | sys.exit(1) 573 | 574 | if len(args) == 0: 575 | self.show_usage(0) 576 | 577 | self.action = args[0] 578 | if self.action not in ('start', 'stop', 'configtest'): 579 | print "Invalid action: %s" % self.action 580 | self.show_usage(1) 581 | 582 | for opt, arg in opts: 583 | if opt in ('-c', '--config'): 584 | self._app.config_file = os.path.abspath(arg) 585 | 586 | elif opt in ('-p', '--pidfile'): 587 | self._app.pidfile_path = arg 588 | 589 | elif opt in ('-f', '--foreground'): 590 | self.detach_process = False 591 | self._app.stdout_path = '/dev/tty' 592 | self._app.stderr_path = '/dev/tty' 593 | self._app.foreground = True 594 | 595 | else: 596 | print "Could not parse option: %s" % opt 597 | self.show_usage(1) 598 | 599 | def getLogFileHandles(logger): 600 | handles = [] 601 | for handler in logger.handlers: 602 | handles.append(handler.stream.fileno()) 603 | if logger.parent: 604 | handles += getLogFileHandles(logger.parent) 605 | return handles 606 | 607 | def main(): 608 | app = NginxNewRelicAgent() 609 | daemon_runner = MyDaemonRunner(app) 610 | 611 | try: 612 | from setproctitle import setproctitle 613 | setproctitle('nginx-nr-agent') 614 | except ImportError: 615 | pass 616 | 617 | if not os.path.isfile(app.config_file) or not os.access(app.config_file, os.R_OK): 618 | print "Config file %s could not be found or opened." % app.config_file 619 | sys.exit(1) 620 | 621 | try: 622 | logging.config.fileConfig(app.config_file, None, False) 623 | except Exception, e: 624 | print "Error while configuring logging: %s" % e 625 | sys.exit(1) 626 | 627 | if not app.foreground: 628 | daemon_runner.daemon_context.files_preserve = getLogFileHandles(LOG) 629 | daemon_runner.do_action() 630 | elif daemon_runner.action == 'configtest': 631 | app.configtest() 632 | else: 633 | app.run() 634 | 635 | if __name__ == '__main__': 636 | main() 637 | --------------------------------------------------------------------------------