Skip to content

Commit

Permalink
Merge pull request #1 from wikical/master
Browse files Browse the repository at this point in the history
Some cleanup plus possibility to specify different graph_category
  • Loading branch information
lambdaq committed Sep 9, 2014
2 parents bd4b703 + 5789648 commit f031d05
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 111 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.swp
*.py[cod]

# C extensions
Expand Down
25 changes: 25 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Copyright (c) 2012 Github user "stevetu"
Copyright (c) 2013 Antonis Christofides
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* The names of its contributors may not be used to endorse or promote
products derived from this software without specific prior written
permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 changes: 0 additions & 38 deletions README.md

This file was deleted.

50 changes: 50 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
munin-uwsgi-stats
=================

Detailed uwsgi stats plugin for munin. In order to get the stats it
uses both the uwsgi `stats server`_ and the operating system's process
list (for stuff that's not available through the stats server, or
wherever the stats server sucks).

Installation
------------

::

pip install psutil
wget -O /usr/share/munin/plugins/uwsgi_ https://github.com/wikical/munin-uwsgi-stats/raw/master/uwsgi_
chmod 755 /usr/share/munin/plugins/uwsgi_

Configuration
-------------

1. add the `stats server`_ to your uWSGI config and restart uWSGI (for
example, use 127.0.0.1:4999; try "nc 127.0.0.1 4999" to see if
there's any JSON output).

.. _stats server: http://projects.unbit.it/uwsgi/wiki/StatsServer

2. edit `/etc/munin/plugin-conf.d/munin-node`::

[uwsgi_*]
user root
env.port 4999
env.name My_uwsgi

The port can be either a TCP port or the filename to a unix domain
socket. The uwsgi server must run on localhost; if the specified
port is a TCP port, the host is assumed to be 127.0.0.1.

If you are monitoring more than one uwsgi applications you can
specify many ports and names, like this::

[uwsgi_*]
user root
env.port 4999 4998 4997
env.name app1 app2 app3


License
-------

Modified BSD - see file LICENSE for details
181 changes: 108 additions & 73 deletions uwsgi_
Original file line number Diff line number Diff line change
@@ -1,96 +1,131 @@
#!/usr/bin/env python

# coding: utf8
import os
import sys
import json
import socket
import psutil
from itertools import izip_longest

# your uwsgi stat socket addr/path here

"""
/etc/munin/plugin-conf.d/munin-node
[uwsgi_*]
env.addr 127.0.0.1:4999
"""

UWSGI_STATS = ''

import os, os.path, sys, time, datetime, json, re, socket
from pprint import pformat

UWSGI_STATS = UWSGI_STATS or os.environ.get('addr', '')
if not UWSGI_STATS:
raise Exception('uWSGI stats config not found. %s' % pformat(os.environ))


if ':' in UWSGI_STATS:
t = UWSGI_STATS.split(':')
sfamily, addr = (socket.AF_INET, (t[0], int(t[1])) )
else:
sfamily, addr = (socket.AF_UNIX, UWSGI_STATS)

name, p, mode = os.path.basename(__file__).partition('_')

modes = {
MODES = {
'listen_queue': 'listen queue',
'listen_queue_errors': 'listen queue errors',

'requests': 'requests per ${graph_period}',
'requests': 'requests per ${graph_period}',
'exceptions': 'exceptions',
'vsz': 'physical memory',
'tx': 'send bytes',
'avg_rt': 'average response time (ms)',
'rss': 'physical memory',
}



if len(sys.argv)>1:
opt = sys.argv[1]

if opt == 'suggest':
print '\n'.join(modes.keys())
elif opt == 'install':
p = os.path.realpath(__file__)
for x in modes.keys():
os.system('ln -s %s /etc/munin/plugins/%s_%s' % (p, name, x))
elif opt == 'config' and mode in modes:
out = ('graph_title %(desc)s\n'
'graph_vlabel %(desc)s\n'
'graph_category uWSGI\n')
if mode in ['tx', 'vsz']:
out += 'graph_args --base 1024\n'
def config(apps):
out = ('graph_title {0}\n'
'graph_vlabel {0}\n'
'graph_category {1}\n').format(MODES[mode], 'uwsgi')
if mode in ['tx', 'rss']:
out += 'graph_args --base 1024\n'
for app in apps:
out += "{0}.label {0}\n".format(app["name"], mode)
if mode in ['requests', 'exceptions', 'tx']:
out += '%(name)s.type DERIVE\n'
out += '%(name)s.min 0\n'
out += "%(name)s.label %(name)s"
out = out % {'name': mode, 'desc': modes[mode]}
print out
else:
out += ('{0}.type DERIVE\n'
'{0}.min 0\n').format(app["name"])
return out


def get_data_from_uwsgi_stats_server(app):
"""Return a json object with stuff returned by the uwsgi stats server.
"""
s = socket.socket(app['socket_family'], socket.SOCK_STREAM)
s.settimeout(3)
s.connect(app['socket_address'])
js = ''
try:
s = socket.socket(sfamily, socket.SOCK_STREAM)
s.settimeout(3)
s.connect( addr )

while True:
data = s.recv(4096)
if len(data) < 1:
break
js += data
except:
import traceback
traceback.print_exc()
raise Exception("unable to get uWSGI stats at %s" % UWSGI_STATS)

dd = json.loads(js or '{}')
workers = dd.get('workers', [])
while True:
data = s.recv(4096)
if len(data) < 1:
break
js += data
s.close()
return json.loads(js or '{}')


def get_value_from_uwsgi_stats_server(app, mode):
dd = get_data_from_uwsgi_stats_server(app)
workers = dd.get('workers', [])
if mode in ['listen_queue', 'listen_queue_errors']:
value = dd.get(mode, 0)
elif mode in ['avg_rt']:
value = sum([x.get(mode, 0)/1000 for x in workers]) / len(workers)
value = sum([x.get(mode, 0) / 1000 for x in workers]) / len(workers)
else:
value = sum([x.get(mode, 0) for x in workers])
print '%s.value %s' % (mode, value)
return value


def get_master(app, master_processes):
"""Return the master process for app given the set of all master processes.
The master process to return is the one that has open an appropriate
connection. If not found, returns None.
"""
for m in master_processes:
for c in m.get_connections():
addr = c.local_address
if (len(addr) == 2 and str(addr[1]) == app["port"]) or (
addr == app["port"]):
return m
return None


def get_uwsgi_processes(apps):
"""Read the process table and add related data to apps.
Adds two items in each app: master_process (a Process object)
and worker_processes (a list of Process objects).
"""
master_processes = []
for p in psutil.process_iter():
if p.cmdline[0] == 'uWSGI master':
master_processes.append(p)
for app in apps:
app['master_process'] = get_master(app, master_processes)
app['worker_processes'] = app['master_process'].get_children()


def get_rss(app):
result = sum([x.get_memory_info().rss for x in app['worker_processes']])
result += app['master_process'].get_memory_info().rss
return result


ports = os.environ.get('port', '').split()
names = os.environ.get('name', 'uwsgi').split()

apps = [{'name': name} for name in names]
for app, port in izip_longest(apps, ports, fillvalue=''):
app['port'] = port
if port.isdigit():
app['socket_family'] = socket.AF_INET
app['socket_address'] = ('127.0.0.1', int(port))
elif port:
app['socket_family'] = socket.AF_UNIX
app['socket_address'] = port
else:
app['socket_family'] = None
app['socket_address'] = None

name, p, mode = os.path.basename(__file__).partition('_')

if mode == '':
mode = 'requests'

if len(sys.argv) > 1 and sys.argv[1] == 'config':
sys.stdout.write(config(apps))
sys.exit(0)

if mode == 'rss':
get_uwsgi_processes(apps)

for app in apps:
if mode != 'rss':
result = get_value_from_uwsgi_stats_server(app, mode)
else:
result = get_rss(app)
print('{0}.value {1}'.format(app["name"], result))

0 comments on commit f031d05

Please sign in to comment.