access.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. #!/usr/bin/env python
  2. # Licensed to Cloudera, Inc. under one
  3. # or more contributor license agreements. See the NOTICE file
  4. # distributed with this work for additional information
  5. # regarding copyright ownership. Cloudera, Inc. licenses this file
  6. # to you under the Apache License, Version 2.0 (the
  7. # "License"); you may not use this file except in compliance
  8. # with the License. You may obtain a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. """
  18. Decorators and methods related to access log.
  19. This assumes a single-threaded server.
  20. """
  21. import logging
  22. import re
  23. import threading
  24. import time
  25. import desktop.conf
  26. ACCESS_LOG = logging.getLogger('access')
  27. def access_log_level(lvl):
  28. """Decorator to set the access log level of a view function."""
  29. if lvl not in (logging.DEBUG, logging.WARN, logging.ERROR, logging.CRITICAL, logging.FATAL):
  30. raise ValueError('%s is not a valid logging level' % (lvl,))
  31. def deco_view(func):
  32. func.access_log_level = lvl
  33. return func
  34. return deco_view
  35. #
  36. # Keep most recent per user per app per view access info
  37. #
  38. # This is a dictionary (indexed by user)
  39. # of dictionary (indexed by app)
  40. # of dictionary (indexed by path)
  41. # of list (of AccessInfo) sorted by time most recent first
  42. #
  43. recent_access_map = { }
  44. _recent_access_map_lk = threading.Lock()
  45. _per_user_lk = { } # Indexed by username
  46. # Store a map of usernames and a dictionary of
  47. # their IP addresses and last access times
  48. last_access_map = { }
  49. # Max number of records per user per view to keep
  50. _USER_ACCESS_HISTORY_SIZE = desktop.conf.USER_ACCESS_HISTORY_SIZE.get()
  51. class AccessInfo(dict):
  52. """
  53. Represents details on a user access.
  54. In addition to the attributes specified in __init__, it may contain
  55. ``msg`` -- A message associated with the access
  56. ``app`` -- The top level package name of the view function, which
  57. need NOT be a valid Desktop application name
  58. """
  59. def __init__(self, request):
  60. self['username'] = request.user.username or '-anon-'
  61. if request.META.has_key('HTTP_X_FORWARDED_FOR'):
  62. self['remote_ip'] = request.META.get('HTTP_X_FORWARDED_FOR', '-')
  63. else:
  64. self['remote_ip'] = request.META.get('REMOTE_ADDR', '-')
  65. self['method'] = request.method
  66. self['path'] = request.path
  67. self['proto'] = request.META.get('SERVER_PROTOCOL', '-')
  68. self['agent'] = request.META.get('HTTP_USER_AGENT', '-')
  69. self['time'] = time.time()
  70. def log(self, level, msg=None):
  71. if msg is not None:
  72. self['msg'] = msg
  73. ACCESS_LOG.log(level,
  74. '%(remote_ip)s %(username)s - "%(method)s %(path)s %(proto)s" -- %(msg)s' %
  75. self)
  76. else:
  77. ACCESS_LOG.log(level,
  78. '%(remote_ip)s %(username)s - "%(method)s %(path)s %(proto)s"' % self)
  79. def add_to_access_history(self, app):
  80. """Record this user access to the recent access map"""
  81. self['app'] = app
  82. user = self['username']
  83. path = self['path']
  84. try:
  85. app_dict = recent_access_map[user]
  86. except KeyError:
  87. # Hold the global lock when modifying recent_access_map
  88. _recent_access_map_lk.acquire()
  89. try:
  90. app_dict = { }
  91. _per_user_lk[user] = threading.Lock()
  92. recent_access_map[user] = app_dict
  93. finally:
  94. _recent_access_map_lk.release()
  95. # Hold the per user lock when modifying adding the access record.
  96. # We could further break down the locking granularity but that seems silly.
  97. user_lk = _per_user_lk[user]
  98. user_lk.acquire()
  99. try:
  100. try:
  101. path_dict = app_dict[app]
  102. except KeyError:
  103. path_dict = { }
  104. app_dict[app] = path_dict
  105. try:
  106. view_access_list = path_dict[path]
  107. except KeyError:
  108. view_access_list = [ ]
  109. path_dict[path] = view_access_list
  110. # Most recent first
  111. view_access_list.insert(0, self)
  112. if len(view_access_list) > _USER_ACCESS_HISTORY_SIZE:
  113. view_access_list.pop()
  114. # Update the IP address and last access time of the user
  115. last_access_map[user] = {'ip':self['remote_ip'],
  116. 'time':self['time']}
  117. finally:
  118. user_lk.release()
  119. _MODULE_RE = re.compile('[^.]*')
  120. def log_page_hit(request, view_func, level=None):
  121. """Log the request to the access log"""
  122. if level is None:
  123. level = logging.INFO
  124. ai = AccessInfo(request)
  125. ai.log(level)
  126. # Find the app
  127. app_re_match = _MODULE_RE.match(view_func.__module__)
  128. app = app_re_match and app_re_match.group(0) or '-'
  129. ai.add_to_access_history(app)
  130. def access_log(request, msg=None, level=None):
  131. """
  132. access_log(request, msg=None, level=None) -> None
  133. Write to the access log. This could be a page hit, or general auditing information.
  134. """
  135. if level is None:
  136. level = logging.INFO
  137. ai = AccessInfo(request)
  138. ai.log(level, msg)
  139. def access_warn(request, msg=None):
  140. """Write to access log with a WARN log level"""
  141. ai = AccessInfo(request)
  142. ai.log(logging.WARN, msg)