Skip to content

Commit c3803c1

Browse files
committed
- Support SSL connection to Zabbix server using client certificate;
- Make script compatible with Python 3; - Add support for arguments; - Add support to skip a data collector;
1 parent 008be39 commit c3803c1

File tree

1 file changed

+156
-110
lines changed

1 file changed

+156
-110
lines changed

zbx_nginx_stats.py

Lines changed: 156 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,50 @@
1-
#!/usr/bin/python
1+
#!/usr/bin/env python
22

3-
import urllib2, base64, re, struct, time, socket, sys, datetime, os.path
3+
from __future__ import absolute_import
4+
from __future__ import division
5+
from __future__ import print_function
6+
from __future__ import unicode_literals
7+
8+
import sys
9+
is_python_3 = sys.version_info > (3, 0)
10+
11+
import argparse, base64, re, struct, time, socket, datetime, os.path
12+
13+
if is_python_3:
14+
import urllib.request as urllib # Python 3
15+
import codecs
16+
else:
17+
import urllib2 as urllib # Python 2
418

519
try:
620
import json
721
except:
822
import simplejson as json
923

10-
zabbix_host = '127.0.0.1' # Zabbix server IP
11-
zabbix_port = 10051 # Zabbix server port
12-
hostname = 'Zabbix Agent' # Name of monitored host, like it shows in zabbix web ui
13-
time_delta = 1 # grep interval in minutes
14-
15-
# URL to nginx stat (http_stub_status_module)
16-
stat_url = 'https://nginx.server/nginx_stat'
17-
18-
# Nginx log file path
19-
nginx_log_file_path = '/var/log/nginx/access.log'
20-
21-
# Optional Basic Auth
22-
username = 'user'
23-
password = 'pass'
24+
parser = argparse.ArgumentParser()
25+
parser.add_argument('--use-ssl', default=False, action="store_true", help='Enable SSL')
26+
parser.add_argument('--ssl-cafile', default='/etc/zabbix/certs/cacert.pem', type=str, help='CA PEM file path')
27+
parser.add_argument('--ssl-certfile', default='/etc/zabbix/certs/agent.crt', type=str, help='Client/Agent certificate file path')
28+
parser.add_argument('--ssl-keyfile', default='/etc/zabbix/certs/agent.key', type=str, help='Client/Agent private key file path')
29+
parser.add_argument('--zabbix-host', default='127.0.0.1', type=str, help='Zabbix server host')
30+
parser.add_argument('--zabbix-port', default=10051, type=int, help='Zabbix server port')
31+
parser.add_argument('--monitored-hostname', default='Zabbix agent', type=str, help='Name of monitored host, like it shows in zabbix web ui')
32+
parser.add_argument('--nginx-auth-username', default=None, type=str, help='Nginx authentication username')
33+
parser.add_argument('--nginx-auth-password', default=None, type=str, help='Nginx authentication password')
34+
parser.add_argument('--nginx-status-module-url', default='http://127.0.0.1:55777/nginx-status', type=str, help='URL to retrieve Nginx status (http_stub_status_module)')
35+
parser.add_argument('--nginx-accesslog', default='/var/log/nginx/access.log', type=str, help='Nginx access log file path')
36+
parser.add_argument('--print-metric', default=-1, type=int, help='Print an specific metric')
37+
parser.add_argument('--skip-nginx-accesslog', default=False, action="store_true", help='Don\'t gather data from the Nginx access log')
38+
parser.add_argument('--skip-nginx-status-module', default=False, action="store_true", help='Don\'t gather data from the Nginx status module (http_stub_status_module)')
39+
40+
args = parser.parse_args()
41+
42+
# grep interval in minutes
43+
time_delta = 1
2444

2545
# Temp file, with log file cursor position
2646
seek_file = '/tmp/nginx_log_stat'
2747

28-
2948
class Metric(object):
3049
def __init__(self, host, key, value, clock=None):
3150
self.host = host
@@ -38,81 +57,106 @@ def __repr__(self):
3857
return 'Metric(%r, %r, %r)' % (self.host, self.key, self.value)
3958
return 'Metric(%r, %r, %r, %r)' % (self.host, self.key, self.value, self.clock)
4059

41-
def send_to_zabbix(metrics, zabbix_host='127.0.0.1', zabbix_port=10051):
42-
60+
def send_to_zabbix(metrics, zabbix_host, zabbix_port):
4361
j = json.dumps
4462
metrics_data = []
4563
for m in metrics:
4664
clock = m.clock or ('%d' % time.time())
47-
metrics_data.append(('{"host":%s,"key":%s,"value":%s,"clock":%s}') % (j(m.host), j(m.key), j(m.value), j(clock)))
48-
json_data = ('{"request":"sender data","data":[%s]}') % (','.join(metrics_data))
49-
data_len = struct.pack('<Q', len(json_data))
50-
packet = 'ZBXD\x01'+ data_len + json_data
65+
#print('%s, %s, %s, %s' % (m.host, m.key, m.value, clock))
66+
metrics_data.append('{"host":%s,"key":%s,"value":%s,"clock":%s}' % (j(m.host), j(m.key), j(m.value), j(clock)))
5167

52-
#print packet
53-
#print ':'.join(x.encode('hex') for x in packet)
68+
# Zabbix 3.0 protocol - https://www.zabbix.com/documentation/3.0/manual/appendix/items/activepassive
69+
data = ('{"request":"sender data","data":[%s]}' % (','.join(metrics_data)))
70+
data_length = len(data)
71+
data_header = struct.pack('<Q', data_length) # 8 bytes
72+
data_to_send = b'ZBXD\x01' + data_header + data.encode('utf-8')
73+
#print(repr(data_to_send))
5474

5575
try:
5676
zabbix = socket.socket()
57-
zabbix.connect((zabbix_host, zabbix_port))
58-
zabbix.sendall(packet)
59-
resp_hdr = _recv_all(zabbix, 13)
60-
if not resp_hdr.startswith('ZBXD\x01') or len(resp_hdr) != 13:
61-
print 'Wrong zabbix response'
77+
sock = zabbix
78+
if args.use_ssl:
79+
import ssl
80+
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
81+
context.options |= ssl.OP_NO_SSLv2
82+
context.options |= ssl.OP_NO_SSLv3
83+
context.verify_mode = ssl.CERT_REQUIRED
84+
context.load_verify_locations(cafile=args.ssl_cafile)
85+
context.load_cert_chain(certfile=args.ssl_certfile, keyfile=args.ssl_keyfile)
86+
87+
sock = context.wrap_socket(zabbix, server_hostname=zabbix_host)
88+
89+
sock.connect((zabbix_host, zabbix_port))
90+
sock.sendall(data_to_send)
91+
92+
resp_hdr = _recv_all(sock, 13)
93+
if not resp_hdr.decode('ascii').startswith('ZBXD\x01') or len(resp_hdr) != 13:
94+
print('Wrong zabbix response')
6295
return False
96+
6397
resp_body_len = struct.unpack('<Q', resp_hdr[5:])[0]
64-
resp_body = zabbix.recv(resp_body_len)
65-
zabbix.close()
98+
resp_body = sock.recv(resp_body_len).decode('ascii')
99+
100+
#sock.shutdown(socket.SHUT_RDWR)
101+
sock.close()
66102

67103
resp = json.loads(resp_body)
68-
#print resp
104+
#print(resp)
69105
if resp.get('response') != 'success':
70-
print 'Got error from Zabbix: %s' % resp
106+
print('Got error from Zabbix: %s' % resp)
71107
return False
72108
return True
73-
except:
74-
print 'Error while sending data to Zabbix'
109+
except Exception as e:
110+
print('Error while sending data to Zabbix %s:' % e)
111+
#sock.shutdown(socket.SHUT_RDWR)
112+
sock.close()
75113
return False
76114

77-
78115
def _recv_all(sock, count):
79-
buf = ''
116+
buf = b''
80117
while len(buf)<count:
81118
chunk = sock.recv(count-len(buf))
82119
if not chunk:
83120
return buf
84121
buf += chunk
85-
return buf
86-
122+
return buf # type(buf) == bytes
87123

88124
def get(url, login, passwd):
89-
req = urllib2.Request(url)
90-
if login and passwd:
91-
base64string = base64.encodestring('%s:%s' % (login, passwd)).replace('\n', '')
92-
req.add_header("Authorization", "Basic %s" % base64string)
93-
q = urllib2.urlopen(req)
94-
res = q.read()
95-
q.close()
96-
return res
125+
req = urllib.Request(url)
126+
if login and passwd:
127+
base64string = base64.encodestring('%s:%s' % (login, passwd)).replace('\n', '')
128+
req.add_header("Authorization", "Basic %s" % base64string)
129+
q = urllib.urlopen(req)
130+
if is_python_3:
131+
res = codecs.decode(q.read(), 'utf-8')
132+
else:
133+
res = q.read().decode('utf-8')
134+
q.close()
135+
return res
97136

98137
def parse_nginx_stat(data):
99-
a = {}
100-
# Active connections
101-
a['active_connections'] = re.match(r'(.*):\s(\d*)', data[0], re.M | re.I).group(2)
102-
# Accepts
103-
a['accepted_connections'] = re.match(r'\s(\d*)\s(\d*)\s(\d*)', data[2], re.M | re.I).group(1)
104-
# Handled
105-
a['handled_connections'] = re.match(r'\s(\d*)\s(\d*)\s(\d*)', data[2], re.M | re.I).group(2)
106-
# Requests
107-
a['handled_requests'] = re.match(r'\s(\d*)\s(\d*)\s(\d*)', data[2], re.M | re.I).group(3)
108-
# Reading
109-
a['header_reading'] = re.match(r'(.*):\s(\d*)(.*):\s(\d*)(.*):\s(\d*)', data[3], re.M | re.I).group(2)
110-
# Writing
111-
a['body_reading'] = re.match(r'(.*):\s(\d*)(.*):\s(\d*)(.*):\s(\d*)', data[3], re.M | re.I).group(4)
112-
# Waiting
113-
a['keepalive_connections'] = re.match(r'(.*):\s(\d*)(.*):\s(\d*)(.*):\s(\d*)', data[3], re.M | re.I).group(6)
114-
return a
138+
a = {}
139+
140+
# Active connections
141+
a['active_connections'] = re.match(r'(.*):\s(\d*)', data[0], re.M | re.I).group(2)
142+
# Accepts
143+
a['accepted_connections'] = re.match(r'\s(\d*)\s(\d*)\s(\d*)', data[2], re.M | re.I).group(1)
144+
# Handled
145+
a['handled_connections'] = re.match(r'\s(\d*)\s(\d*)\s(\d*)', data[2], re.M | re.I).group(2)
146+
# Requests
147+
a['handled_requests'] = re.match(r'\s(\d*)\s(\d*)\s(\d*)', data[2], re.M | re.I).group(3)
148+
# Reading
149+
a['header_reading'] = re.match(r'(.*):\s(\d*)(.*):\s(\d*)(.*):\s(\d*)', data[3], re.M | re.I).group(2)
150+
# Writing
151+
a['body_reading'] = re.match(r'(.*):\s(\d*)(.*):\s(\d*)(.*):\s(\d*)', data[3], re.M | re.I).group(4)
152+
# Waiting
153+
a['keepalive_connections'] = re.match(r'(.*):\s(\d*)(.*):\s(\d*)(.*):\s(\d*)', data[3], re.M | re.I).group(6)
154+
155+
if not is_python_3:
156+
for key, value in a.iteritems():
157+
a[key] = value.decode('utf-8')
115158

159+
return a
116160

117161
def read_seek(file):
118162
if os.path.isfile(file):
@@ -131,66 +175,68 @@ def write_seek(file, value):
131175
f.write(value)
132176
f.close()
133177

178+
#print('[12/Mar/2014:03:21:13 +0400]')
134179

135-
#print '[12/Mar/2014:03:21:13 +0400]'
136-
137-
d = datetime.datetime.now()-datetime.timedelta(minutes=time_delta)
138-
minute = int(time.mktime(d.timetuple()) / 60)*60
139-
d = d.strftime('%d/%b/%Y:%H:%M')
140-
141-
total_rps = 0
142-
rps = [0]*60
143-
tps = [0]*60
144-
res_code = {}
180+
if not args.skip_nginx_accesslog:
181+
d = datetime.datetime.now()-datetime.timedelta(minutes=time_delta)
182+
minute = int(time.mktime(d.timetuple()) / 60)*60
183+
d = d.strftime('%d/%b/%Y:%H:%M')
145184

146-
nf = open(nginx_log_file_path, 'r')
185+
total_rps = 0
186+
rps = [0]*60
187+
tps = [0]*60
188+
res_code = {}
147189

148-
new_seek = seek = read_seek(seek_file)
190+
nf = open(args.nginx_accesslog, 'r')
149191

150-
# if new log file, don't do seek
151-
if os.path.getsize(nginx_log_file_path) > seek:
152-
nf.seek(seek)
192+
new_seek = seek = read_seek(seek_file)
153193

154-
line = nf.readline()
155-
while line:
156-
if d in line:
157-
new_seek = nf.tell()
158-
total_rps += 1
159-
sec = int(re.match('(.*):(\d+):(\d+):(\d+)\s', line).group(4))
160-
code = re.match(r'(.*)"\s(\d*)\s', line).group(2)
161-
if code in res_code:
162-
res_code[code] += 1
163-
else:
164-
res_code[code] = 1
194+
# if new log file, don't do seek
195+
if os.path.getsize(args.nginx_accesslog) > seek:
196+
nf.seek(seek)
165197

166-
rps[sec] += 1
167198
line = nf.readline()
168-
169-
if total_rps != 0:
170-
write_seek(seek_file, str(new_seek))
171-
172-
nf.close()
173-
174-
metric = (len(sys.argv) >= 2) and re.match(r'nginx\[(.*)\]', sys.argv[1], re.M | re.I).group(1) or False
175-
data = get(stat_url, username, password).split('\n')
199+
while line:
200+
if d in line:
201+
new_seek = nf.tell()
202+
total_rps += 1
203+
sec = int(re.match('(.*):(\d+):(\d+):(\d+)\s', line).group(4))
204+
code = re.match(r'(.*)"\s(\d*)\s', line).group(2)
205+
if code in res_code:
206+
res_code[code] += 1
207+
else:
208+
res_code[code] = 1
209+
210+
rps[sec] += 1
211+
line = nf.readline()
212+
213+
if total_rps != 0:
214+
write_seek(seek_file, str(new_seek))
215+
216+
nf.close()
217+
218+
should_print_metric = (args.print_metric != -1) and re.match(r'nginx\[(.*)\]', args.debug_metric, re.M | re.I).group(1) or False
219+
data = get(args.nginx_status_module_url, args.nginx_auth_username, args.nginx_auth_password).split('\n')
176220
data = parse_nginx_stat(data)
177221

178222
data_to_send = []
179223

180224
# Adding the metrics to response
181-
if not metric:
225+
if should_print_metric:
226+
print(data[metric])
227+
228+
if not args.skip_nginx_status_module:
182229
for i in data:
183-
data_to_send.append(Metric(hostname, ('nginx[%s]' % i), data[i]))
184-
else:
185-
print data[metric]
230+
data_to_send.append(Metric(args.monitored_hostname, ('nginx[%s]' % i), data[i]))
186231

187-
# Adding the request per seconds to response
188-
for t in range(0,60):
189-
data_to_send.append(Metric(hostname, 'nginx[rps]', rps[t], minute+t))
232+
if not args.skip_nginx_accesslog:
233+
# Add the request per seconds to response
234+
for t in range(0,60):
235+
data_to_send.append(Metric(args.monitored_hostname, 'nginx[rps]', rps[t], minute+t))
190236

191-
# Adding the response codes stats to respons
192-
for t in res_code:
193-
data_to_send.append(Metric(hostname, ('nginx[%s]' % t), res_code[t]))
237+
# Add the response codes stats to response
238+
for t in res_code:
239+
data_to_send.append(Metric(args.monitored_hostname, ('nginx[%s]' % t), res_code[t]))
194240

195241

196-
send_to_zabbix(data_to_send, zabbix_host, zabbix_port)
242+
send_to_zabbix(data_to_send, args.zabbix_host, args.zabbix_port)

0 commit comments

Comments
 (0)