Improved logging for requests, responses and events.

- The following logging namespaces are now used, to make it easier to
  filter only log lines related to the logged application message:

  - `platypush:events`
  - `platypush:requests`
  - `platypush:responses`

- Those messages are always logged as JSON, with no prefixes nor
  suffixes.

- Requests are always logged when executed - no more delegation to the
  upstream backend.

- Responses are always logged when fully populated (including `id`,
  `origin`, `target` etc.), instead of being logged when still partially
  populated. This makes it particularly easy to link request/response
  IDs directly from the logs.
This commit is contained in:
Fabio Manganiello 2023-07-15 01:01:15 +02:00
parent cb04af0bbd
commit fa1364d854
Signed by: blacklight
GPG key ID: D90FBA7F76362774
4 changed files with 32 additions and 65 deletions

View file

@ -21,20 +21,18 @@ def execute_route():
try: try:
msg = json.loads(request.data.decode('utf-8')) msg = json.loads(request.data.decode('utf-8'))
except Exception as e: except Exception as e:
logger().error( logger().error('Unable to parse JSON from request %s: %s', request.data, e)
'Unable to parse JSON from request {}: {}'.format(request.data, str(e))
)
abort(400, str(e)) abort(400, str(e))
logger().info('Received message on the HTTP backend: {}'.format(msg)) logger().debug(
'Received message on the HTTP backend from %s: %s', request.remote_addr, msg
)
try: try:
response = send_message(msg) response = send_message(msg)
return Response(str(response or {}), mimetype='application/json') return Response(str(response or {}), mimetype='application/json')
except Exception as e: except Exception as e:
logger().error( logger().error('Error while running HTTP action: %s. Request: %s', e, msg)
'Error while running HTTP action: {}. Request: {}'.format(str(e), msg)
)
abort(500, str(e)) abort(500, str(e))

View file

@ -57,6 +57,7 @@ class Event(Message):
self.type = f'{self.__class__.__module__}.{self.__class__.__name__}' self.type = f'{self.__class__.__module__}.{self.__class__.__name__}'
self.args = kwargs self.args = kwargs
self.disable_web_clients_notification = disable_web_clients_notification self.disable_web_clients_notification = disable_web_clients_notification
self._logger = logging.getLogger('platypush:events')
for arg, value in self.args.items(): for arg, value in self.args.items():
if arg not in [ if arg not in [

View file

@ -57,6 +57,7 @@ class Request(Message):
self.args = args if args else {} self.args = args if args else {}
self.backend = backend self.backend = backend
self.token = token self.token = token
self._logger = logging.getLogger('platypush:requests')
@classmethod @classmethod
def build(cls, msg): def build(cls, msg):
@ -77,16 +78,11 @@ class Request(Message):
@staticmethod @staticmethod
def _generate_id(): def _generate_id():
_id = '' return ''.join(f'{random.randint(0, 255):02x}' for _ in range(0, 16))
for _ in range(0, 16):
_id += '%.2x' % random.randint(0, 255)
return _id
def _execute_procedure(self, *args, **kwargs): def _execute_procedure(self, *args, **kwargs):
from platypush.config import Config
from platypush.procedure import Procedure from platypush.procedure import Procedure
logger.info('Executing procedure request: {}'.format(self.action))
procedures = Config.get_procedures() procedures = Config.get_procedures()
proc_name = '.'.join(self.action.split('.')[1:]) proc_name = '.'.join(self.action.split('.')[1:])
if proc_name not in procedures: if proc_name not in procedures:
@ -120,7 +116,7 @@ class Request(Message):
constants = Config.get_constants() constants = Config.get_constants()
context['constants'] = {} context['constants'] = {}
for (name, value) in constants.items(): for name, value in constants.items():
context['constants'][name] = value context['constants'][name] = value
keys = [] keys = []
@ -143,23 +139,20 @@ class Request(Message):
@classmethod @classmethod
def expand_value_from_context(cls, _value, **context): def expand_value_from_context(cls, _value, **context):
for (k, v) in context.items(): for k, v in context.items():
if isinstance(v, Message): if isinstance(v, Message):
v = json.loads(str(v)) v = json.loads(str(v))
try: try:
exec('{}={}'.format(k, v)) exec('{}={}'.format(k, v))
except Exception as e: except Exception:
logger.debug(str(e))
if isinstance(v, str): if isinstance(v, str):
try: try:
exec('{}="{}"'.format(k, re.sub(r'(^|[^\\])"', '\1\\"', v))) exec('{}="{}"'.format(k, re.sub(r'(^|[^\\])"', '\1\\"', v)))
except Exception as e: except Exception as e2:
logger.debug( logger.debug(
'Could not set context variable {}={}: {}'.format( 'Could not set context variable %s=%s: %s', k, v, e2
k, v, str(e)
) )
) logger.debug('Context: %s', context)
logger.debug('Context: {}'.format(context))
parsed_value = '' parsed_value = ''
if not isinstance(_value, str): if not isinstance(_value, str):
@ -205,6 +198,7 @@ class Request(Message):
response.id = self.id response.id = self.id
response.target = self.origin response.target = self.origin
response.origin = Config.get('device_id') response.origin = Config.get('device_id')
response.log()
if self.backend and self.origin: if self.backend and self.origin:
self.backend.send_response(response=response, request=self) self.backend.send_response(response=response, request=self)
@ -235,6 +229,7 @@ class Request(Message):
from platypush.plugins import RunnablePlugin from platypush.plugins import RunnablePlugin
response = None response = None
self.log()
try: try:
if self.action.startswith('procedure.'): if self.action.startswith('procedure.'):
@ -243,25 +238,20 @@ class Request(Message):
if response is not None: if response is not None:
self._send_response(response) self._send_response(response)
return response return response
# utils.get_context is a special action that simply returns the current context # utils.get_context is a special action that simply returns the current context
elif self.action == 'utils.get_context': if self.action == 'utils.get_context':
response = Response(output=context) response = Response(output=context)
self._send_response(response) self._send_response(response)
return response return response
else:
action = self.expand_value_from_context(self.action, **context) action = self.expand_value_from_context(self.action, **context)
(module_name, method_name) = get_module_and_method_from_action( (module_name, method_name) = get_module_and_method_from_action(action)
action
)
plugin = get_plugin(module_name) plugin = get_plugin(module_name)
assert plugin, f'No such plugin: {module_name}' assert plugin, f'No such plugin: {module_name}'
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
msg = 'Uncaught pre-processing exception from action [{}]: {}'.format( response = Response(output=None, errors=[str(e)])
self.action, str(e)
)
logger.warning(msg)
response = Response(output=None, errors=[msg])
self._send_response(response) self._send_response(response)
return response return response
@ -276,41 +266,23 @@ class Request(Message):
else: else:
response = plugin.run(method_name, args) response = plugin.run(method_name, args)
if not response: if response is None:
logger.warning( response = Response()
'Received null response from action {}'.format(action)
)
else:
if response.is_error():
logger.warning(
(
'Response processed with errors from ' + 'action {}: {}'
).format(action, str(response))
)
else:
response.log(action)
except (AssertionError, TimeoutError) as e: except (AssertionError, TimeoutError) as e:
logger.warning( logger.warning(
'%s from action [%s]: %s', e.__class__.__name__, action, str(e) '%s from action [%s]: %s', e.__class__.__name__, action, e
) )
response = Response(output=None, errors=[str(e)]) response = Response(output=None, errors=[str(e)])
except Exception as e: except Exception as e:
# Retry mechanism # Retry mechanism
plugin.logger.exception(e) plugin.logger.exception(e)
logger.warning(
(
'Uncaught exception while processing response '
+ 'from action [{}]: {}'
).format(action, str(e))
)
errors = errors or [] errors = errors or []
if str(e) not in errors: if str(e) not in errors:
errors.append(str(e)) errors.append(str(e))
response = Response(output=None, errors=errors) response = Response(output=None, errors=errors)
if _n_tries - 1 > 0: if _n_tries - 1 > 0:
logger.info('Reloading plugin {} and retrying'.format(module_name)) logger.info('Reloading plugin %s and retrying', module_name)
plugin = get_plugin(module_name, reload=True) plugin = get_plugin(module_name, reload=True)
if isinstance(plugin, RunnablePlugin): if isinstance(plugin, RunnablePlugin):
plugin.bus = get_bus() plugin.bus = get_bus()

View file

@ -37,6 +37,7 @@ class Response(Message):
self.errors = self._parse_msg(errors or []) self.errors = self._parse_msg(errors or [])
self.origin = origin self.origin = origin
self.id = id self.id = id
self._logger = logging.getLogger('platypush:responses')
def is_error(self): def is_error(self):
"""Returns True if the response has errors""" """Returns True if the response has errors"""
@ -98,14 +99,9 @@ class Response(Message):
return json.dumps(response_dict, cls=self.Encoder) return json.dumps(response_dict, cls=self.Encoder)
def log(self, action=None): def log(self, *args, **kwargs):
prefix = ( self.logging_level = logging.WARNING if self.is_error() else logging.INFO
f'Processed response from action {action}: ' super().log(*args, **kwargs)
if action
else 'Received response: '
)
super().log(prefix)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et: