3
3
import os
4
4
import yaml
5
5
from argparse import ArgumentParser
6
- from prometheus_client import make_wsgi_app
7
- from wsgiref .simple_server import make_server , WSGIServer
8
- from prometheus_client .core import GaugeMetricFamily , CounterMetricFamily , REGISTRY
6
+ from prometheus_client .core import GaugeMetricFamily , CounterMetricFamily
7
+ from prometheus_client import generate_latest , CONTENT_TYPE_LATEST
9
8
import time
10
9
import logging
11
- import threading
12
10
from P4 import P4
13
11
from collections import defaultdict
14
- from socketserver import ThreadingMixIn
12
+ import urllib .parse as urlparse
13
+ from http .server import BaseHTTPRequestHandler , HTTPServer
14
+ from socketserver import ForkingMixIn
15
15
16
16
17
17
COMMAND_STATES = {
26
26
27
27
class P4Collector (object ):
28
28
29
- def __init__ (self , config ):
30
- self .config = config
31
- self .p4_pool = defaultdict (dict )
29
+ def __init__ (self ):
30
+ self .p4_pool = {}
31
+ self .params = {}
32
+ self .config = {}
32
33
33
34
def connection (self , p4port ):
34
35
tid = threading .current_thread ().ident
35
- if p4port not in self .p4_pool or tid not in self . p4_pool [ p4port ] :
36
+ if p4port not in self .p4_pool :
36
37
p4 = P4 (exception_level = 1 , prog = 'prometheus-p4-metrics' )
37
38
hostname , port = p4port .split (':' )
38
39
credentials = self .config .get ('credentials' , {}).get (p4port , None )
@@ -53,13 +54,13 @@ def connection(self, p4port):
53
54
except Exception as e :
54
55
logging .error ('Failed to log in to %s: %s' , p4port , e )
55
56
credentials = None
56
- self .p4_pool [p4port ][ tid ] = (p4 , credentials is not None )
57
+ self .p4_pool [p4port ] = (p4 , credentials is not None )
57
58
except Exception as e :
58
59
logging .error ('Failed to connect to %s: %s' , p4port , e )
59
60
return None , False
60
61
else :
61
62
logging .debug ('Using cached connection for %s/%s' , p4port , tid )
62
- return self .p4_pool [p4port ][ tid ]
63
+ return self .p4_pool [p4port ]
63
64
64
65
def name (self , name ):
65
66
return 'p4_' + name
@@ -157,7 +158,7 @@ def depot_guages(self, p4):
157
158
created_guage .add_metric ([depot_name , depot_type ], int (depot ['time' ]))
158
159
return size_guage , count_guage , created_guage
159
160
160
- def collect (self , params ):
161
+ def collect (self ):
161
162
if not params :
162
163
return
163
164
p4port = params ['target' ][0 ]
@@ -205,26 +206,72 @@ def collect(self, params):
205
206
yield created_guage
206
207
207
208
208
- class ThreadedWSGIServer ( ThreadingMixIn , WSGIServer ):
209
+ class ForkingHTTPServer ( ForkingMixIn , HTTPServer ):
209
210
pass
210
211
211
212
213
+ class P4ExporterHandler (BaseHTTPRequestHandler ):
214
+ def __init__ (self , config_path , * args , ** kwargs ):
215
+ BaseHTTPRequestHandler .__init__ (self , * args , ** kwargs )
216
+ self ._config_path = config_path
217
+ self ._collector = P4Collector ()
218
+ logging .info ('New collector created' )
219
+
220
+ def collect (self , config , params ):
221
+ with open (self ._config_path ) as f :
222
+ config = yaml .safe_load (f )
223
+ self ._collector .config = config
224
+ self ._collector .params = params
225
+ registry = CollectorRegistry ()
226
+ registry .register (self ._collector )
227
+ return generate_latest (registry )
228
+
229
+ def do_GET (self ):
230
+ logging .verbose ('Got request...' )
231
+ url = urlparse .urlparse (self .path )
232
+ if url .path == '/metrics' :
233
+ params = urlparse .parse_qs (url .query )
234
+ if 'target' not in params :
235
+ self .send_response (400 )
236
+ self .end_headers ()
237
+ self .wfile .write ("Missing 'target' from parameters" )
238
+ return
239
+
240
+ try :
241
+ output = self .collect ()
242
+ self .send_response (200 )
243
+ self .send_header ('Content-Type' , CONTENT_TYPE_LATEST )
244
+ self .end_headers ()
245
+ self .wfile .write (output )
246
+ except Exception as e :
247
+ logging .error ('Internal error: %s' , e )
248
+ self .send_response (500 )
249
+ self .end_headers ()
250
+ self .wfile .write (traceback .format_exc ())
251
+
252
+ elif url .path == '/' :
253
+ self .send_response (200 )
254
+ self .end_headers ()
255
+ self .wfile .write ("""<html>
256
+ <head><title>SNMP Exporter</title></head>
257
+ <body>
258
+ <h1>SNMP Exporter</h1>
259
+ <p>Visit <code>/metrics?address=1.2.3.4</code> to use.</p>
260
+ </body>
261
+ </html>""" )
262
+ else :
263
+ self .send_response (404 )
264
+ self .end_headers ()
265
+
266
+
212
267
if __name__ == '__main__' :
213
268
parser = ArgumentParser ()
214
269
parser .add_argument ('-v' , '--verbose' , dest = 'verbose' , action = 'store_true' , default = True , help = 'Enable verbose logging' )
215
270
parser .add_argument ('-p' , '--port' , dest = 'port' , type = int , default = 9666 , help = 'The port to expose metrics on, default: 9666' )
216
271
parser .add_argument ('-c' , '--config' , dest = 'config' , default = '/etc/p4_exporter/conf.yml' , help = 'Path to the configuration file' )
217
272
options = parser .parse_args ()
218
273
logging .basicConfig (level = logging .DEBUG if options .verbose else logging .INFO , format = '[%(levelname)s] %(message)s' )
219
- logging .info ('Creating collector...' )
220
- if os .path .isfile (options .config ):
221
- config = yaml .load (open (options .config , 'r' ))
222
- else :
223
- logging .warning ("Config file %s does not exist, no credentials loaded." , options .config )
224
- config = {}
225
- REGISTRY .register (P4Collector (config ))
226
- logging .info ('Loaded %d credentials' , len (config .get ('credentials' , [])))
274
+ handler = lambda * args , ** kwargs : P4ExporterHandler (options .config , * args , ** kwargs )
227
275
logging .info ('Listening on port :%d...' , options .port )
228
- app = make_wsgi_app ()
229
- httpd = make_server ('' , options .port , app , server_class = ThreadedWSGIServer )
230
- httpd .serve_forever ()
276
+ server = ForkingHTTPServer (('' , options .port ), handler )
277
+ server .serve_forever ()
0 commit comments