xref: /aosp_15_r20/external/autotest/site_utils/log_socket_server.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# The source code is from following Python documentation:
3*9c5db199SXin Li# https://docs.python.org/2/howto/logging-cookbook.html#network-logging
4*9c5db199SXin Li
5*9c5db199SXin Li# Classes in this file are used to create a simple TCP socket-based logging
6*9c5db199SXin Li# receiver. The receiver listens to default logging port (9020) and save log to
7*9c5db199SXin Li# any given log configuration, e.g., a local file. Once the receiver is running,
8*9c5db199SXin Li# client can add a logging handler to write log to the receiver with following
9*9c5db199SXin Li# sample code:
10*9c5db199SXin Li# socketHandler = logging.handlers.SocketHandler('localhost',
11*9c5db199SXin Li#         logging.handlers.DEFAULT_TCP_LOGGING_PORT)
12*9c5db199SXin Li# logging.getLogger().addHandler(socketHandler)
13*9c5db199SXin Li
14*9c5db199SXin Lifrom __future__ import absolute_import
15*9c5db199SXin Lifrom __future__ import division
16*9c5db199SXin Lifrom __future__ import print_function
17*9c5db199SXin Li
18*9c5db199SXin Liimport ctypes
19*9c5db199SXin Liimport pickle
20*9c5db199SXin Liimport logging
21*9c5db199SXin Liimport multiprocessing
22*9c5db199SXin Liimport select
23*9c5db199SXin Liimport six.moves.socketserver
24*9c5db199SXin Liimport struct
25*9c5db199SXin Liimport time
26*9c5db199SXin Li
27*9c5db199SXin Liimport common
28*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils
29*9c5db199SXin Li
30*9c5db199SXin Liclass LogRecordStreamHandler(six.moves.socketserver.StreamRequestHandler):
31*9c5db199SXin Li    """Handler for a streaming logging request.
32*9c5db199SXin Li
33*9c5db199SXin Li    This basically logs the record using whatever logging policy is
34*9c5db199SXin Li    configured locally.
35*9c5db199SXin Li    """
36*9c5db199SXin Li
37*9c5db199SXin Li    def handle(self):
38*9c5db199SXin Li        """
39*9c5db199SXin Li        Handle multiple requests - each expected to be a 4-byte length,
40*9c5db199SXin Li        followed by the LogRecord in pickle format. Logs the record
41*9c5db199SXin Li        according to whatever policy is configured locally.
42*9c5db199SXin Li        """
43*9c5db199SXin Li        while True:
44*9c5db199SXin Li            chunk = self.connection.recv(4)
45*9c5db199SXin Li            if len(chunk) < 4:
46*9c5db199SXin Li                return
47*9c5db199SXin Li            slen = struct.unpack('>L', chunk)[0]
48*9c5db199SXin Li            chunk = self.connection.recv(slen)
49*9c5db199SXin Li            while len(chunk) < slen:
50*9c5db199SXin Li                chunk = chunk + self.connection.recv(slen - len(chunk))
51*9c5db199SXin Li            obj = self.unpickle(chunk)
52*9c5db199SXin Li            record = logging.makeLogRecord(obj)
53*9c5db199SXin Li            self.handle_log_record(record)
54*9c5db199SXin Li
55*9c5db199SXin Li
56*9c5db199SXin Li    def unpickle(self, data):
57*9c5db199SXin Li        """Unpickle data received.
58*9c5db199SXin Li
59*9c5db199SXin Li        @param data: Received data.
60*9c5db199SXin Li        @returns: unpickled data.
61*9c5db199SXin Li        """
62*9c5db199SXin Li        return pickle.loads(data)
63*9c5db199SXin Li
64*9c5db199SXin Li
65*9c5db199SXin Li    def handle_log_record(self, record):
66*9c5db199SXin Li        """Process log record.
67*9c5db199SXin Li
68*9c5db199SXin Li        @param record: log record.
69*9c5db199SXin Li        """
70*9c5db199SXin Li        # if a name is specified, we use the named logger rather than the one
71*9c5db199SXin Li        # implied by the record.
72*9c5db199SXin Li        if self.server.logname is not None:
73*9c5db199SXin Li            name = self.server.logname
74*9c5db199SXin Li        else:
75*9c5db199SXin Li            name = record.name
76*9c5db199SXin Li        logger = logging.getLogger(name)
77*9c5db199SXin Li        # N.B. EVERY record gets logged. This is because Logger.handle
78*9c5db199SXin Li        # is normally called AFTER logger-level filtering. If you want
79*9c5db199SXin Li        # to do filtering, do it at the client end to save wasting
80*9c5db199SXin Li        # cycles and network bandwidth!
81*9c5db199SXin Li        logger.handle(record)
82*9c5db199SXin Li
83*9c5db199SXin Li
84*9c5db199SXin Liclass LogRecordSocketReceiver(six.moves.socketserver.ThreadingTCPServer):
85*9c5db199SXin Li    """Simple TCP socket-based logging receiver.
86*9c5db199SXin Li    """
87*9c5db199SXin Li
88*9c5db199SXin Li    allow_reuse_address = 1
89*9c5db199SXin Li
90*9c5db199SXin Li    def __init__(self, host='localhost', port=None,
91*9c5db199SXin Li                 handler=LogRecordStreamHandler):
92*9c5db199SXin Li        if not port:
93*9c5db199SXin Li            port = utils.get_unused_port()
94*9c5db199SXin Li        six.moves.socketserver.ThreadingTCPServer.__init__(self, (host, port), handler)
95*9c5db199SXin Li        self.abort = 0
96*9c5db199SXin Li        self.timeout = 1
97*9c5db199SXin Li        self.logname = None
98*9c5db199SXin Li        self.port = port
99*9c5db199SXin Li
100*9c5db199SXin Li
101*9c5db199SXin Li    def serve_until_stopped(self):
102*9c5db199SXin Li        """Run the socket receiver until aborted."""
103*9c5db199SXin Li        print('Log Record Socket Receiver is started.')
104*9c5db199SXin Li        abort = 0
105*9c5db199SXin Li        while not abort:
106*9c5db199SXin Li            rd, wr, ex = select.select([self.socket.fileno()], [], [],
107*9c5db199SXin Li                                       self.timeout)
108*9c5db199SXin Li            if rd:
109*9c5db199SXin Li                self.handle_request()
110*9c5db199SXin Li            abort = self.abort
111*9c5db199SXin Li        print('Log Record Socket Receiver is stopped.')
112*9c5db199SXin Li
113*9c5db199SXin Li
114*9c5db199SXin Liclass LogSocketServer:
115*9c5db199SXin Li    """A wrapper class to start and stop a TCP server for logging."""
116*9c5db199SXin Li
117*9c5db199SXin Li    process = None
118*9c5db199SXin Li    port = None
119*9c5db199SXin Li
120*9c5db199SXin Li    @staticmethod
121*9c5db199SXin Li    def start(**kwargs):
122*9c5db199SXin Li        """Start Log Record Socket Receiver in a new process.
123*9c5db199SXin Li
124*9c5db199SXin Li        @param kwargs: log configuration, e.g., format, filename.
125*9c5db199SXin Li
126*9c5db199SXin Li        @raise Exception: if TCP server is already running.
127*9c5db199SXin Li        """
128*9c5db199SXin Li        if LogSocketServer.process:
129*9c5db199SXin Li            raise Exception('Log Record Socket Receiver is already running.')
130*9c5db199SXin Li        server_started = multiprocessing.Value(ctypes.c_bool, False)
131*9c5db199SXin Li        port = multiprocessing.Value(ctypes.c_int, 0)
132*9c5db199SXin Li        LogSocketServer.process = multiprocessing.Process(
133*9c5db199SXin Li                target=LogSocketServer._start_server,
134*9c5db199SXin Li                args=(server_started, port),
135*9c5db199SXin Li                kwargs=kwargs)
136*9c5db199SXin Li        LogSocketServer.process.start()
137*9c5db199SXin Li        while not server_started.value:
138*9c5db199SXin Li            time.sleep(0.1)
139*9c5db199SXin Li        LogSocketServer.port = port.value
140*9c5db199SXin Li        print('Log Record Socket Server is started at port %d.' % port.value)
141*9c5db199SXin Li
142*9c5db199SXin Li
143*9c5db199SXin Li    @staticmethod
144*9c5db199SXin Li    def _start_server(server_started, port, **kwargs):
145*9c5db199SXin Li        """Start the TCP server to receive log.
146*9c5db199SXin Li
147*9c5db199SXin Li        @param server_started: True if socket log server is started.
148*9c5db199SXin Li        @param port: Port used by socket log server.
149*9c5db199SXin Li        @param kwargs: log configuration, e.g., format, filename.
150*9c5db199SXin Li        """
151*9c5db199SXin Li        # Clear all existing log handlers.
152*9c5db199SXin Li        logging.getLogger().handlers = []
153*9c5db199SXin Li        if not kwargs:
154*9c5db199SXin Li            logging.basicConfig(
155*9c5db199SXin Li                    format='%(asctime)s - %(levelname)s - %(message)s')
156*9c5db199SXin Li        else:
157*9c5db199SXin Li            logging.basicConfig(**kwargs)
158*9c5db199SXin Li
159*9c5db199SXin Li        tcp_server = LogRecordSocketReceiver()
160*9c5db199SXin Li        print('Starting TCP server...')
161*9c5db199SXin Li        server_started.value = True
162*9c5db199SXin Li        port.value = tcp_server.port
163*9c5db199SXin Li        tcp_server.serve_until_stopped()
164*9c5db199SXin Li
165*9c5db199SXin Li
166*9c5db199SXin Li    @staticmethod
167*9c5db199SXin Li    def stop():
168*9c5db199SXin Li        """Stop Log Record Socket Receiver.
169*9c5db199SXin Li        """
170*9c5db199SXin Li        if LogSocketServer.process:
171*9c5db199SXin Li            LogSocketServer.process.terminate()
172*9c5db199SXin Li            LogSocketServer.process = None
173*9c5db199SXin Li            LogSocketServer.port = None
174