git_trace2_event_log.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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 Write(self, path=None):
  109. """Writes the log out to a file.
  110. Log is only written if 'path' or 'git config --get trace2.eventtarget'
  111. provide a valid path to write logs to.
  112. Logging filename format follows the git trace2 style of being a unique
  113. (exclusive writable) file.
  114. Args:
  115. path: Path to where logs should be written.
  116. Returns:
  117. log_path: Path to the log file if log is written, otherwise None
  118. """
  119. log_path = None
  120. # If no logging path is specified, get the path from 'trace2.eventtarget'.
  121. if path is None:
  122. cmd = ['config', '--get', 'trace2.eventtarget']
  123. # TODO(https://crbug.com/gerrit/13706): Use GitConfig when it supports
  124. # system git config variables.
  125. p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True,
  126. bare=True)
  127. retval = p.Wait()
  128. if retval == 0:
  129. # Strip trailing carriage-return in path.
  130. path = p.stdout.rstrip('\n')
  131. elif retval != 1:
  132. # `git config --get` is documented to produce an exit status of `1` if
  133. # the requested variable is not present in the configuration. Report any
  134. # other return value as an error.
  135. print("repo: error: 'git config --get' call failed with return code: %r, stderr: %r" % (
  136. retval, p.stderr), file=sys.stderr)
  137. if isinstance(path, str):
  138. # Get absolute path.
  139. path = os.path.abspath(os.path.expanduser(path))
  140. else:
  141. raise TypeError('path: str required but got %s.' % type(path))
  142. # Git trace2 requires a directory to write log to.
  143. # TODO(https://crbug.com/gerrit/13706): Support file (append) mode also.
  144. if not os.path.isdir(path):
  145. return None
  146. # Use NamedTemporaryFile to generate a unique filename as required by git trace2.
  147. try:
  148. with tempfile.NamedTemporaryFile(mode='x', prefix=self._sid, dir=path,
  149. delete=False) as f:
  150. # TODO(https://crbug.com/gerrit/13706): Support writing events as they
  151. # occur.
  152. for e in self._log:
  153. # Dump in compact encoding mode.
  154. # See 'Compact encoding' in Python docs:
  155. # https://docs.python.org/3/library/json.html#module-json
  156. json.dump(e, f, indent=None, separators=(',', ':'))
  157. f.write('\n')
  158. log_path = f.name
  159. except FileExistsError as err:
  160. print('repo: warning: git trace2 logging failed: %r' % err,
  161. file=sys.stderr)
  162. return None
  163. return log_path