error_utils.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. """
  2. Utils for handling of errors
  3. """
  4. import re
  5. import sys
  6. import traceback
  7. from flask import Flask, jsonify, current_app, request
  8. from werkzeug.exceptions import (
  9. HTTPException,
  10. InternalServerError,
  11. BadRequest,
  12. NotFound,
  13. Gone,
  14. )
  15. from sqlalchemy.orm import Query
  16. def log_error(exc: Exception, error_msg: str):
  17. """Collect meta data about the exception and log it.
  18. error_msg comes in as an extra attribute because Exception implementations differ here.
  19. """
  20. exc_info = sys.exc_info()
  21. last_traceback = exc_info[2]
  22. if hasattr(exc, "__cause__") and exc.__cause__ is not None:
  23. exc_info = (exc.__cause__.__class__, exc.__cause__, last_traceback)
  24. extra = dict(url=request.path, **get_err_source_info(last_traceback))
  25. msg = (
  26. '{error_name}:"{message}" [occurred at {src_module}({src_func}):{src_linenr},'
  27. "URL was: {url}]".format(
  28. error_name=exc.__class__.__name__, message=error_msg, **extra
  29. )
  30. )
  31. current_app.logger.error(msg, exc_info=exc_info)
  32. def get_err_source_info(original_traceback=None) -> dict:
  33. """Use this when an error is handled to get info on where it occurred."""
  34. try: # carefully try to get the actual place where the error happened
  35. if not original_traceback:
  36. original_traceback = sys.exc_info()[2] # class, exc, traceback
  37. first_call = traceback.extract_tb(original_traceback)[-1]
  38. return dict(
  39. src_module=first_call[0],
  40. src_linenr=first_call[1],
  41. src_func=first_call[2],
  42. src_code=first_call[3],
  43. )
  44. except Exception as e:
  45. current_app.warning(
  46. "I was unable to retrieve error source information: %s." % str(e)
  47. )
  48. return dict(module="", linenr=0, method="", src_code="")
  49. def error_handling_router(error: HTTPException):
  50. """
  51. Generic handler for errors.
  52. We respond in json if the request content-type is JSON.
  53. The ui package can also define how it wants to render HTML errors, by setting a function.
  54. """
  55. log_error(error, getattr(error, "description", str(error)))
  56. http_error_code = 500 # fallback
  57. if hasattr(error, "code"):
  58. try:
  59. http_error_code = int(error.code)
  60. except (ValueError, TypeError): # if code is not an int or None
  61. pass
  62. error_text = getattr(
  63. error, "description", f"Something went wrong: {error.__class__.__name__}"
  64. )
  65. if request.is_json or (
  66. request.url_rule is not None and request.url_rule.rule.startswith("/api")
  67. ):
  68. response = jsonify(
  69. dict(
  70. message=getattr(error, "description", str(error)),
  71. status=http_error_code,
  72. )
  73. )
  74. response.status_code = http_error_code
  75. return response
  76. # Can UI handle this specific type?
  77. elif hasattr(current_app, "%s_handler_html" % error.__class__.__name__):
  78. return getattr(current_app, "%s_handler_html" % error.__class__.__name__)(error)
  79. # Can UI handle HTTPException? Let's make one from the error.
  80. elif hasattr(current_app, "HttpException_handler_html"):
  81. return current_app.HttpException_handler_html(error)
  82. # This fallback is ugly but better than nothing.
  83. else:
  84. return "%s:%s" % (error.__class__.__name__, error_text), http_error_code
  85. def add_basic_error_handlers(app: Flask):
  86. """
  87. Register classes we care about with the generic handler.
  88. See also the auth package for auth-specific error handling (Unauthorized, Forbidden)
  89. """
  90. app.register_error_handler(InternalServerError, error_handling_router)
  91. app.register_error_handler(BadRequest, error_handling_router)
  92. app.register_error_handler(HTTPException, error_handling_router)
  93. app.register_error_handler(NotFound, error_handling_router)
  94. app.register_error_handler(Gone, error_handling_router)
  95. app.register_error_handler(Exception, error_handling_router)
  96. def print_query(query: Query) -> str:
  97. """Print full SQLAlchemy query with compiled parameters.
  98. Recommended use as developer tool only.
  99. Adapted from https://stackoverflow.com/a/63900851/13775459
  100. """
  101. regex = re.compile(r":(?P<name>\w+)")
  102. params = query.statement.compile().params
  103. sql = regex.sub(r"'{\g<name>}'", str(query.statement)).format(**params)
  104. from flexmeasures.data import db
  105. print(f"\nPrinting SQLAlchemy query to database {db.engine.url.database}:\n\n")
  106. print(sql)
  107. return sql