1
1
import argparse
2
2
import signal
3
3
import time
4
+ import threading
4
5
from multiprocessing .connection import Listener
5
6
from django_sql_sniffer import analyzer , injector , sniffer
6
7
7
8
9
+ class DjangoSQLListener (threading .Thread ):
10
+ def __init__ (self , cmdline_args ):
11
+ super ().__init__ (name = self .__class__ .__name__ )
12
+ self .analyzer = analyzer .SQLAnalyzer (tail = cmdline_args .tail , top = cmdline_args .number , by_sum = cmdline_args .sum , by_count = cmdline_args .count )
13
+ self .logger = sniffer .configure_logger (__name__ , cmdline_args .verbose )
14
+ self ._target_pid = cmdline_args .pid
15
+ self ._verbose = cmdline_args .verbose
16
+ self ._listener = None
17
+ self ._conn = None
18
+ self ._running = False
19
+
20
+ def _create_socket (self ):
21
+ self ._listener = Listener (('localhost' , 0 )) # will force OS to assign a random available port
22
+ _ , self .port = self ._listener .address
23
+ self .logger .debug (f"listening on port { self .port } " )
24
+
25
+ def _inject_sniffer (self ):
26
+ with open (sniffer .__file__ , "r" ) as source_file :
27
+ source_code = source_file .read ()
28
+ code_to_inject = source_code .replace ("PORT = 0" , f"PORT = { self .port } " )
29
+ code_to_inject = code_to_inject .replace ("LOGGING_ENABLED = False" , f"LOGGING_ENABLED = { self ._verbose } " )
30
+ code_to_inject += "sniffer.start()"
31
+ injector .inject (str (self ._target_pid ), code_to_inject , self ._verbose )
32
+ self .logger .debug ("SQL sniffer injected, waiting for a reply" )
33
+
34
+ def _callback_wait (self ):
35
+ self ._conn = self ._listener .accept ()
36
+ self ._listener .close ()
37
+ self .logger .debug ("reply received, sniffer active" )
38
+
39
+ def start (self ):
40
+ self .logger .debug ("starting" )
41
+ self ._create_socket ()
42
+ self ._inject_sniffer ()
43
+ self ._callback_wait ()
44
+ self ._running = True
45
+ super ().start ()
46
+
47
+ def stop (self , * a , ** kw ):
48
+ self .logger .debug ("stopping" )
49
+ self ._running = False
50
+
51
+ def run (self ):
52
+ while self ._running :
53
+ try :
54
+ if self ._conn .poll (3 ):
55
+ sql_packet = self ._conn .recv ()
56
+ duration = sql_packet ["duration" ]
57
+ sql = sql_packet ["sql" ]
58
+ self .analyzer .record_query (sql , duration )
59
+ except EOFError :
60
+ self .logger .info ("sniffer disconnected, exiting" )
61
+ self ._running = False
62
+ except Exception as e :
63
+ self .logger .error (f"unexpected error: { str (e )} " , exc_info = True )
64
+ self ._running = False
65
+
66
+ self .analyzer .print_summary ()
67
+ self ._conn .close ()
68
+ injector .inject (str (self ._target_pid ), "sniffer.stop()" , self ._verbose )
69
+ self .logger .debug ("done" )
70
+
71
+
8
72
def main ():
9
- # parse arguments
10
73
parser = argparse .ArgumentParser (description = "Analyze SQL queries originating from a running process" )
11
74
parser .add_argument ("-p" , "--pid" , help = "The id of the process executing Django SQL queries" )
12
75
parser .add_argument ("-t" , "--tail" , action = 'store_true' , help = "Log queries as they are executed in tail mode" )
@@ -15,39 +78,15 @@ def main():
15
78
parser .add_argument ("-c" , "--count" , action = 'store_true' , help = "Sort query summary by execution count" )
16
79
parser .add_argument ("-n" , "--number" , type = int , default = 3 , help = "Number of top queries and their stats to display in summary" )
17
80
args = parser .parse_args ()
18
- logger = sniffer .configure_logger (__name__ , args .verbose )
19
-
20
- # create socket and listen for a connection
21
- listener = Listener (('localhost' , 0 )) # will force OS to assign a random available port
22
- _ , port = listener .address
23
- logger .debug (f"listening on port { port } " )
24
-
25
- # inject sniffer monkey patch
26
- with open (sniffer .__file__ , "r" ) as source_file :
27
- source_code = source_file .read ()
28
- code_to_inject = source_code .replace ("PORT = 0" , f"PORT = { port } " )
29
- code_to_inject = code_to_inject .replace ("LOGGING_ENABLED = False" , f"LOGGING_ENABLED = { args .verbose } " )
30
- code_to_inject += "sniffer.start()"
31
- injector .inject (str (args .pid ), code_to_inject , args .verbose )
32
-
33
- # wait for callback
34
- logger .debug ("SQL sniffer injected, waiting for a reply" )
35
- conn = listener .accept ()
36
- listener .close ()
37
-
38
- # receive and analyze executed SQL
39
- logger .debug ("reply received, sniffer active" )
40
- sql_analyzer = analyzer .SQLAnalyzer (conn , verbose = args .verbose , tail = args .tail , top = args .number , by_sum = args .sum , by_count = args .count )
41
- signal .signal (signal .SIGTERM , sql_analyzer .stop )
42
- signal .signal (signal .SIGINT , sql_analyzer .stop )
43
- if hasattr (signal , "SIGINFO" ):
44
- signal .signal (signal .SIGINFO , sql_analyzer .print_summary ) # print summary on ^T, without exiting the process
45
- sql_analyzer .start ()
46
81
47
- while sql_analyzer .is_alive ():
48
- time .sleep (0.5 )
49
- injector .inject (str (args .pid ), "sniffer.stop()" , args .verbose )
50
- sql_analyzer .print_summary ()
82
+ listener = DjangoSQLListener (args )
83
+ listener .start ()
84
+ signal .signal (signal .SIGTERM , listener .stop )
85
+ signal .signal (signal .SIGINT , listener .stop )
86
+ if hasattr (signal , "SIGINFO" ):
87
+ signal .signal (signal .SIGINFO , listener .analyzer .print_summary ) # print summary on ^T, without exiting the process
88
+ while listener .is_alive ():
89
+ time .sleep (1 )
51
90
52
91
53
92
if __name__ == "__main__" :
0 commit comments