/debug_toolbar_mongo/operation_tracker.py

https://github.com/heyman/django-debug-toolbar-mongo
Python | 236 lines | 184 code | 31 blank | 21 comment | 16 complexity | 7ebc67fe981e33c43e5494e212a598f9 MD5 | raw file
  1. import functools
  2. import traceback
  3. import time
  4. import inspect
  5. import os
  6. import SocketServer
  7. import django
  8. from django.conf import settings
  9. import pymongo
  10. import pymongo.collection
  11. import pymongo.cursor
  12. __all__ = ['queries', 'inserts', 'updates', 'removes', 'install_tracker',
  13. 'uninstall_tracker', 'reset']
  14. _original_methods = {
  15. 'insert': pymongo.collection.Collection.insert,
  16. 'update': pymongo.collection.Collection.update,
  17. 'remove': pymongo.collection.Collection.remove,
  18. 'refresh': pymongo.cursor.Cursor._refresh,
  19. }
  20. queries = []
  21. inserts = []
  22. updates = []
  23. removes = []
  24. WANT_STACK_TRACE = getattr(settings, 'DEBUG_TOOLBAR_MONGO_STACKTRACES', True)
  25. def _get_stacktrace():
  26. if WANT_STACK_TRACE:
  27. return _tidy_stacktrace(reversed(inspect.stack()))
  28. else:
  29. return []
  30. # Wrap Cursor._refresh for getting queries
  31. @functools.wraps(_original_methods['insert'])
  32. def _insert(collection_self, doc_or_docs, manipulate=True,
  33. safe=False, check_keys=True, **kwargs):
  34. start_time = time.time()
  35. result = _original_methods['insert'](
  36. collection_self,
  37. doc_or_docs,
  38. manipulate=manipulate,
  39. safe=safe,
  40. check_keys=check_keys,
  41. **kwargs
  42. )
  43. total_time = (time.time() - start_time) * 1000
  44. __traceback_hide__ = True
  45. inserts.append({
  46. 'document': doc_or_docs,
  47. 'safe': safe,
  48. 'time': total_time,
  49. 'stack_trace': _get_stacktrace(),
  50. })
  51. return result
  52. # Wrap Cursor._refresh for getting queries
  53. @functools.wraps(_original_methods['update'])
  54. def _update(collection_self, spec, document, upsert=False,
  55. maniuplate=False, safe=False, multi=False, **kwargs):
  56. start_time = time.time()
  57. result = _original_methods['update'](
  58. collection_self,
  59. spec,
  60. document,
  61. upsert=upsert,
  62. safe=safe,
  63. multi=multi,
  64. **kwargs
  65. )
  66. total_time = (time.time() - start_time) * 1000
  67. __traceback_hide__ = True
  68. updates.append({
  69. 'document': document,
  70. 'upsert': upsert,
  71. 'multi': multi,
  72. 'spec': spec,
  73. 'safe': safe,
  74. 'time': total_time,
  75. 'stack_trace': _get_stacktrace(),
  76. })
  77. return result
  78. # Wrap Cursor._refresh for getting queries
  79. @functools.wraps(_original_methods['remove'])
  80. def _remove(collection_self, spec_or_id, safe=False, **kwargs):
  81. start_time = time.time()
  82. result = _original_methods['remove'](
  83. collection_self,
  84. spec_or_id,
  85. safe=safe,
  86. **kwargs
  87. )
  88. total_time = (time.time() - start_time) * 1000
  89. __traceback_hide__ = True
  90. removes.append({
  91. 'spec_or_id': spec_or_id,
  92. 'safe': safe,
  93. 'time': total_time,
  94. 'stack_trace': _get_stacktrace(),
  95. })
  96. return result
  97. # Wrap Cursor._refresh for getting queries
  98. @functools.wraps(_original_methods['refresh'])
  99. def _cursor_refresh(cursor_self):
  100. # Look up __ private instance variables
  101. def privar(name):
  102. return getattr(cursor_self, '_Cursor__{0}'.format(name))
  103. if privar('id') is not None:
  104. # getMore not query - move on
  105. return _original_methods['refresh'](cursor_self)
  106. # NOTE: See pymongo/cursor.py+557 [_refresh()] and
  107. # pymongo/message.py for where information is stored
  108. # Time the actual query
  109. start_time = time.time()
  110. result = _original_methods['refresh'](cursor_self)
  111. total_time = (time.time() - start_time) * 1000
  112. query_son = privar('query_spec')()
  113. __traceback_hide__ = True
  114. query_data = {
  115. 'time': total_time,
  116. 'operation': 'query',
  117. 'stack_trace': _get_stacktrace(),
  118. }
  119. # Collection in format <db_name>.<collection_name>
  120. collection_name = privar('collection')
  121. query_data['collection'] = collection_name.full_name.split('.')[1]
  122. if query_data['collection'] == '$cmd':
  123. query_data['operation'] = 'command'
  124. # Handle count as a special case
  125. if 'count' in query_son:
  126. # Information is in a different format to a standar query
  127. query_data['collection'] = query_son['count']
  128. query_data['operation'] = 'count'
  129. query_data['skip'] = query_son.get('skip')
  130. query_data['limit'] = query_son.get('limit')
  131. query_data['query'] = query_son['query']
  132. else:
  133. # Normal Query
  134. query_data['skip'] = privar('skip')
  135. query_data['limit'] = privar('limit')
  136. query_data['query'] = query_son['$query']
  137. query_data['ordering'] = _get_ordering(query_son)
  138. queries.append(query_data)
  139. return result
  140. def install_tracker():
  141. if pymongo.collection.Collection.insert != _insert:
  142. pymongo.collection.Collection.insert = _insert
  143. if pymongo.collection.Collection.update != _update:
  144. pymongo.collection.Collection.update = _update
  145. if pymongo.collection.Collection.remove != _remove:
  146. pymongo.collection.Collection.remove = _remove
  147. if pymongo.cursor.Cursor._refresh != _cursor_refresh:
  148. pymongo.cursor.Cursor._refresh = _cursor_refresh
  149. def uninstall_tracker():
  150. if pymongo.collection.Collection.insert == _insert:
  151. pymongo.collection.Collection.insert = _original_methods['insert']
  152. if pymongo.collection.Collection.update == _update:
  153. pymongo.collection.Collection.update = _original_methods['update']
  154. if pymongo.collection.Collection.remove == _remove:
  155. pymongo.collection.Collection.remove = _original_methods['remove']
  156. if pymongo.cursor.Cursor._refresh == _cursor_refresh:
  157. pymongo.cursor.Cursor._refresh = _original_methods['cursor_refresh']
  158. def reset():
  159. global queries, inserts, updates, removes
  160. queries = []
  161. inserts = []
  162. updates = []
  163. removes = []
  164. def _get_ordering(son):
  165. """Helper function to extract formatted ordering from dict.
  166. """
  167. def fmt(field, direction):
  168. return '{0}{1}'.format({-1: '-', 1: '+'}[direction], field)
  169. if '$orderby' in son:
  170. return ', '.join(fmt(f, d) for f, d in son['$orderby'].items())
  171. # Taken from Django Debug Toolbar 0.8.6
  172. def _tidy_stacktrace(stack):
  173. """
  174. Clean up stacktrace and remove all entries that:
  175. 1. Are part of Django (except contrib apps)
  176. 2. Are part of SocketServer (used by Django's dev server)
  177. 3. Are the last entry (which is part of our stacktracing code)
  178. ``stack`` should be a list of frame tuples from ``inspect.stack()``
  179. """
  180. django_path = os.path.realpath(os.path.dirname(django.__file__))
  181. django_path = os.path.normpath(os.path.join(django_path, '..'))
  182. socketserver_path = os.path.realpath(os.path.dirname(SocketServer.__file__))
  183. pymongo_path = os.path.realpath(os.path.dirname(pymongo.__file__))
  184. trace = []
  185. for frame, path, line_no, func_name, text in (f[:5] for f in stack):
  186. s_path = os.path.realpath(path)
  187. # Support hiding of frames -- used in various utilities that provide
  188. # inspection.
  189. if '__traceback_hide__' in frame.f_locals:
  190. continue
  191. if getattr(settings, 'DEBUG_TOOLBAR_CONFIG', {}).get('HIDE_DJANGO_SQL', True) \
  192. and django_path in s_path and not 'django/contrib' in s_path:
  193. continue
  194. if socketserver_path in s_path:
  195. continue
  196. if pymongo_path in s_path:
  197. continue
  198. if not text:
  199. text = ''
  200. else:
  201. text = (''.join(text)).strip()
  202. trace.append((path, line_no, func_name, text))
  203. return trace