git_trace2_event_log.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. # Copyright (C) 2020 The Android Open Source Project
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Provide event logging in the git trace2 EVENT format.
  15. The git trace2 EVENT format is defined at:
  16. https://www.kernel.org/pub/software/scm/git/docs/technical/api-trace2.html#_event_format
  17. https://git-scm.com/docs/api-trace2#_the_event_format_target
  18. Usage:
  19. git_trace_log = EventLog()
  20. git_trace_log.StartEvent()
  21. ...
  22. git_trace_log.ExitEvent()
  23. git_trace_log.Write()
  24. """
  25. import datetime
  26. import json
  27. import os
  28. import sys
  29. import tempfile
  30. import threading
  31. from git_command import GitCommand, RepoSourceVersion
  32. class EventLog(object):
  33. """Event log that records events that occurred during a repo invocation.
  34. Events are written to the log as a consecutive JSON entries, one per line.
  35. Entries follow the git trace2 EVENT format.
  36. Each entry contains the following common keys:
  37. - event: The event name
  38. - sid: session-id - Unique string to allow process instance to be identified.
  39. - thread: The thread name.
  40. - time: is the UTC time of the event.
  41. Valid 'event' names and event specific fields are documented here:
  42. https://git-scm.com/docs/api-trace2#_event_format
  43. """
  44. def __init__(self, env=None):
  45. """Initializes the event log."""
  46. self._log = []
  47. # Try to get session-id (sid) from environment (setup in repo launcher).
  48. KEY = 'GIT_TRACE2_PARENT_SID'
  49. if env is None:
  50. env = os.environ
  51. now = datetime.datetime.utcnow()
  52. # Save both our sid component and the complete sid.
  53. # We use our sid component (self._sid) as the unique filename prefix and
  54. # the full sid (self._full_sid) in the log itself.
  55. self._sid = 'repo-%s-P%08x' % (now.strftime('%Y%m%dT%H%M%SZ'), os.getpid())
  56. parent_sid = env.get(KEY)
  57. # Append our sid component to the parent sid (if it exists).
  58. if parent_sid is not None:
  59. self._full_sid = parent_sid + '/' + self._sid
  60. else:
  61. self._full_sid = self._sid
  62. # Set/update the environment variable.
  63. # Environment handling across systems is messy.
  64. try:
  65. env[KEY] = self._full_sid
  66. except UnicodeEncodeError:
  67. env[KEY] = self._full_sid.encode()
  68. # Add a version event to front of the log.
  69. self._AddVersionEvent()
  70. @property
  71. def full_sid(self):
  72. return self._full_sid
  73. def _AddVersionEvent(self):
  74. """Adds a 'version' event at the beginning of current log."""
  75. version_event = self._CreateEventDict('version')
  76. version_event['evt'] = 2
  77. version_event['exe'] = RepoSourceVersion()
  78. self._log.insert(0, version_event)
  79. def _CreateEventDict(self, event_name):
  80. """Returns a dictionary with the common keys/values for git trace2 events.
  81. Args:
  82. event_name: The event name.
  83. Returns:
  84. Dictionary with the common event fields populated.
  85. """
  86. return {
  87. 'event': event_name,
  88. 'sid': self._full_sid,
  89. 'thread': threading.currentThread().getName(),
  90. 'time': datetime.datetime.utcnow().isoformat() + 'Z',
  91. }
  92. def StartEvent(self):
  93. """Append a 'start' event to the current log."""
  94. start_event = self._CreateEventDict('start')
  95. start_event['argv'] = sys.argv
  96. self._log.append(start_event)
  97. def ExitEvent(self, result):
  98. """Append an 'exit' event to the current log.
  99. Args:
  100. result: Exit code of the event
  101. """
  102. exit_event = self._CreateEventDict('exit')
  103. # Consider 'None' success (consistent with event_log result handling).
  104. if result is None:
  105. result = 0
  106. exit_event['code'] = result
  107. self._log.append(exit_event)
  108. def _GetEventTargetPath(self):
  109. """Get the 'trace2.eventtarget' path from git configuration.
  110. Returns:
  111. path: git config's 'trace2.eventtarget' path if it exists, or None
  112. """
  113. path = None
  114. cmd = ['config', '--get', 'trace2.eventtarget']
  115. # TODO(https://crbug.com/gerrit/13706): Use GitConfig when it supports
  116. # system git config variables.
  117. p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True,
  118. bare=True)
  119. retval = p.Wait()
  120. if retval == 0:
  121. # Strip trailing carriage-return in path.
  122. path = p.stdout.rstrip('\n')
  123. elif retval != 1:
  124. # `git config --get` is documented to produce an exit status of `1` if
  125. # the requested variable is not present in the configuration. Report any
  126. # other return value as an error.
  127. print("repo: error: 'git config --get' call failed with return code: %r, stderr: %r" % (
  128. retval, p.stderr), file=sys.stderr)
  129. return path
  130. def Write(self, path=None):
  131. """Writes the log out to a file.
  132. Log is only written if 'path' or 'git config --get trace2.eventtarget'
  133. provide a valid path to write logs to.
  134. Logging filename format follows the git trace2 style of being a unique
  135. (exclusive writable) file.
  136. Args:
  137. path: Path to where logs should be written.
  138. Returns:
  139. log_path: Path to the log file if log is written, otherwise None
  140. """
  141. log_path = None
  142. # If no logging path is specified, get the path from 'trace2.eventtarget'.
  143. if path is None:
  144. path = self._GetEventTargetPath()
  145. # If no logging path is specified, exit.
  146. if path is None:
  147. return None
  148. if isinstance(path, str):
  149. # Get absolute path.
  150. path = os.path.abspath(os.path.expanduser(path))
  151. else:
  152. raise TypeError('path: str required but got %s.' % type(path))
  153. # Git trace2 requires a directory to write log to.
  154. # TODO(https://crbug.com/gerrit/13706): Support file (append) mode also.
  155. if not os.path.isdir(path):
  156. return None
  157. # Use NamedTemporaryFile to generate a unique filename as required by git trace2.
  158. try:
  159. with tempfile.NamedTemporaryFile(mode='x', prefix=self._sid, dir=path,
  160. delete=False) as f:
  161. # TODO(https://crbug.com/gerrit/13706): Support writing events as they
  162. # occur.
  163. for e in self._log:
  164. # Dump in compact encoding mode.
  165. # See 'Compact encoding' in Python docs:
  166. # https://docs.python.org/3/library/json.html#module-json
  167. json.dump(e, f, indent=None, separators=(',', ':'))
  168. f.write('\n')
  169. log_path = f.name
  170. except FileExistsError as err:
  171. print('repo: warning: git trace2 logging failed: %r' % err,
  172. file=sys.stderr)
  173. return None
  174. return log_path