blob: 0e5e908923f4c9273454264dd79044ba1df9e983 [file] [log] [blame]
Ian Kasprzak30bc3542020-12-23 10:08:20 -08001# 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
15"""Provide event logging in the git trace2 EVENT format.
16
17The git trace2 EVENT format is defined at:
18https://www.kernel.org/pub/software/scm/git/docs/technical/api-trace2.html#_event_format
19https://git-scm.com/docs/api-trace2#_the_event_format_target
20
21 Usage:
22
23 git_trace_log = EventLog()
24 git_trace_log.StartEvent()
25 ...
26 git_trace_log.ExitEvent()
27 git_trace_log.Write()
28"""
29
30
31import datetime
32import json
33import os
34import sys
35import tempfile
36import threading
37
38from git_command import GitCommand, RepoSourceVersion
39
40
41class EventLog(object):
42 """Event log that records events that occurred during a repo invocation.
43
44 Events are written to the log as a consecutive JSON entries, one per line.
45 Entries follow the git trace2 EVENT format.
46
47 Each entry contains the following common keys:
48 - event: The event name
49 - sid: session-id - Unique string to allow process instance to be identified.
50 - thread: The thread name.
51 - time: is the UTC time of the event.
52
53 Valid 'event' names and event specific fields are documented here:
54 https://git-scm.com/docs/api-trace2#_event_format
55 """
56
57 def __init__(self, env=None):
58 """Initializes the event log."""
59 self._log = []
60 # Try to get session-id (sid) from environment (setup in repo launcher).
61 KEY = 'GIT_TRACE2_PARENT_SID'
62 if env is None:
63 env = os.environ
64
65 now = datetime.datetime.utcnow()
66
67 # Save both our sid component and the complete sid.
68 # We use our sid component (self._sid) as the unique filename prefix and
69 # the full sid (self._full_sid) in the log itself.
70 self._sid = 'repo-%s-P%08x' % (now.strftime('%Y%m%dT%H%M%SZ'), os.getpid())
71 parent_sid = env.get(KEY)
72 # Append our sid component to the parent sid (if it exists).
73 if parent_sid is not None:
74 self._full_sid = parent_sid + '/' + self._sid
75 else:
76 self._full_sid = self._sid
77
78 # Set/update the environment variable.
79 # Environment handling across systems is messy.
80 try:
81 env[KEY] = self._full_sid
82 except UnicodeEncodeError:
83 env[KEY] = self._full_sid.encode()
84
85 # Add a version event to front of the log.
86 self._AddVersionEvent()
87
88 @property
89 def full_sid(self):
90 return self._full_sid
91
92 def _AddVersionEvent(self):
93 """Adds a 'version' event at the beginning of current log."""
94 version_event = self._CreateEventDict('version')
Ian Kasprzakfebe73f2021-01-19 09:04:28 -080095 version_event['evt'] = "2"
Ian Kasprzak30bc3542020-12-23 10:08:20 -080096 version_event['exe'] = RepoSourceVersion()
97 self._log.insert(0, version_event)
98
99 def _CreateEventDict(self, event_name):
100 """Returns a dictionary with the common keys/values for git trace2 events.
101
102 Args:
103 event_name: The event name.
104
105 Returns:
106 Dictionary with the common event fields populated.
107 """
108 return {
109 'event': event_name,
110 'sid': self._full_sid,
111 'thread': threading.currentThread().getName(),
112 'time': datetime.datetime.utcnow().isoformat() + 'Z',
113 }
114
115 def StartEvent(self):
116 """Append a 'start' event to the current log."""
117 start_event = self._CreateEventDict('start')
118 start_event['argv'] = sys.argv
119 self._log.append(start_event)
120
121 def ExitEvent(self, result):
122 """Append an 'exit' event to the current log.
123
124 Args:
125 result: Exit code of the event
126 """
127 exit_event = self._CreateEventDict('exit')
128
129 # Consider 'None' success (consistent with event_log result handling).
130 if result is None:
131 result = 0
132 exit_event['code'] = result
133 self._log.append(exit_event)
134
Raman Tennetia5b40a22021-03-16 14:24:14 -0700135 def CommandEvent(self, name, subcommands):
136 """Append a 'command' event to the current log.
137
138 Args:
139 name: Name of the primary command (ex: repo, git)
140 subcommands: List of the sub-commands (ex: version, init, sync)
141 """
142 command_event = self._CreateEventDict('command')
143 command_event['name'] = name
144 command_event['subcommands'] = subcommands
145 self._log.append(command_event)
146
Raman Tenneti7954de12021-07-28 14:36:49 -0700147 def LogConfigEvents(self, config, event_dict_name):
148 """Append a |event_dict_name| event for each config key in |config|.
149
150 Args:
151 config: Configuration dictionary.
152 event_dict_name: Name of the event dictionary for items to be logged under.
153 """
154 for param, value in config.items():
155 event = self._CreateEventDict(event_dict_name)
156 event['param'] = param
157 event['value'] = value
158 self._log.append(event)
159
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800160 def DefParamRepoEvents(self, config):
161 """Append a 'def_param' event for each repo.* config key to the current log.
162
163 Args:
164 config: Repo configuration dictionary
165 """
166 # Only output the repo.* config parameters.
167 repo_config = {k: v for k, v in config.items() if k.startswith('repo.')}
Raman Tenneti7954de12021-07-28 14:36:49 -0700168 self.LogConfigEvents(repo_config, 'def_param')
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800169
Raman Tennetibbe88362021-09-27 15:15:47 -0700170 def GetDataEventName(self, value):
171 """Returns 'data-json' if the value is an array else returns 'data'."""
172 return 'data-json' if value[0] == '[' and value[-1] == ']' else 'data'
Raman Tenneti956f7362021-09-27 10:55:44 -0700173
Raman Tenneti6448a4f2021-09-13 17:40:07 -0700174 def LogDataConfigEvents(self, config, prefix):
175 """Append a 'data' event for each config key/value in |config| to the current log.
176
177 For each keyX and valueX of the config, "key" field of the event is '|prefix|/keyX'
178 and the "value" of the "key" field is valueX.
179
180 Args:
181 config: Configuration dictionary.
182 prefix: Prefix for each key that is logged.
183 """
184 for key, value in config.items():
Raman Tennetibbe88362021-09-27 15:15:47 -0700185 event = self._CreateEventDict(self.GetDataEventName(value))
Raman Tenneti6448a4f2021-09-13 17:40:07 -0700186 event['key'] = f'{prefix}/{key}'
187 event['value'] = value
188 self._log.append(event)
189
Raman Tennetifc7aa902021-06-08 17:41:08 -0700190 def ErrorEvent(self, msg, fmt):
191 """Append a 'error' event to the current log."""
192 error_event = self._CreateEventDict('error')
193 error_event['msg'] = msg
194 error_event['fmt'] = fmt
195 self._log.append(error_event)
196
Ian Kasprzak343d5852021-01-06 16:26:31 -0800197 def _GetEventTargetPath(self):
198 """Get the 'trace2.eventtarget' path from git configuration.
199
200 Returns:
201 path: git config's 'trace2.eventtarget' path if it exists, or None
202 """
203 path = None
204 cmd = ['config', '--get', 'trace2.eventtarget']
205 # TODO(https://crbug.com/gerrit/13706): Use GitConfig when it supports
206 # system git config variables.
207 p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True,
208 bare=True)
209 retval = p.Wait()
210 if retval == 0:
211 # Strip trailing carriage-return in path.
212 path = p.stdout.rstrip('\n')
213 elif retval != 1:
214 # `git config --get` is documented to produce an exit status of `1` if
215 # the requested variable is not present in the configuration. Report any
216 # other return value as an error.
217 print("repo: error: 'git config --get' call failed with return code: %r, stderr: %r" % (
218 retval, p.stderr), file=sys.stderr)
219 return path
220
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800221 def Write(self, path=None):
222 """Writes the log out to a file.
223
224 Log is only written if 'path' or 'git config --get trace2.eventtarget'
225 provide a valid path to write logs to.
226
227 Logging filename format follows the git trace2 style of being a unique
228 (exclusive writable) file.
229
230 Args:
231 path: Path to where logs should be written.
232
233 Returns:
234 log_path: Path to the log file if log is written, otherwise None
235 """
236 log_path = None
237 # If no logging path is specified, get the path from 'trace2.eventtarget'.
238 if path is None:
Ian Kasprzak343d5852021-01-06 16:26:31 -0800239 path = self._GetEventTargetPath()
240
241 # If no logging path is specified, exit.
242 if path is None:
243 return None
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800244
245 if isinstance(path, str):
246 # Get absolute path.
247 path = os.path.abspath(os.path.expanduser(path))
248 else:
249 raise TypeError('path: str required but got %s.' % type(path))
250
251 # Git trace2 requires a directory to write log to.
252
253 # TODO(https://crbug.com/gerrit/13706): Support file (append) mode also.
254 if not os.path.isdir(path):
255 return None
256 # Use NamedTemporaryFile to generate a unique filename as required by git trace2.
257 try:
258 with tempfile.NamedTemporaryFile(mode='x', prefix=self._sid, dir=path,
259 delete=False) as f:
260 # TODO(https://crbug.com/gerrit/13706): Support writing events as they
261 # occur.
262 for e in self._log:
263 # Dump in compact encoding mode.
264 # See 'Compact encoding' in Python docs:
265 # https://docs.python.org/3/library/json.html#module-json
266 json.dump(e, f, indent=None, separators=(',', ':'))
267 f.write('\n')
268 log_path = f.name
269 except FileExistsError as err:
270 print('repo: warning: git trace2 logging failed: %r' % err,
271 file=sys.stderr)
272 return None
273 return log_path