blob: 7426aba90b16ae8d870ef6c361f8c643b5d4b1e0 [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
Josh Steadmon244c9a72022-03-08 10:24:43 -080032import errno
Ian Kasprzak30bc3542020-12-23 10:08:20 -080033import json
34import os
Josh Steadmon244c9a72022-03-08 10:24:43 -080035import socket
Ian Kasprzak30bc3542020-12-23 10:08:20 -080036import sys
37import tempfile
38import threading
39
40from git_command import GitCommand, RepoSourceVersion
41
42
43class EventLog(object):
44 """Event log that records events that occurred during a repo invocation.
45
46 Events are written to the log as a consecutive JSON entries, one per line.
47 Entries follow the git trace2 EVENT format.
48
49 Each entry contains the following common keys:
50 - event: The event name
51 - sid: session-id - Unique string to allow process instance to be identified.
52 - thread: The thread name.
53 - time: is the UTC time of the event.
54
55 Valid 'event' names and event specific fields are documented here:
56 https://git-scm.com/docs/api-trace2#_event_format
57 """
58
59 def __init__(self, env=None):
60 """Initializes the event log."""
61 self._log = []
62 # Try to get session-id (sid) from environment (setup in repo launcher).
63 KEY = 'GIT_TRACE2_PARENT_SID'
64 if env is None:
65 env = os.environ
66
67 now = datetime.datetime.utcnow()
68
69 # Save both our sid component and the complete sid.
70 # We use our sid component (self._sid) as the unique filename prefix and
71 # the full sid (self._full_sid) in the log itself.
72 self._sid = 'repo-%s-P%08x' % (now.strftime('%Y%m%dT%H%M%SZ'), os.getpid())
73 parent_sid = env.get(KEY)
74 # Append our sid component to the parent sid (if it exists).
75 if parent_sid is not None:
76 self._full_sid = parent_sid + '/' + self._sid
77 else:
78 self._full_sid = self._sid
79
80 # Set/update the environment variable.
81 # Environment handling across systems is messy.
82 try:
83 env[KEY] = self._full_sid
84 except UnicodeEncodeError:
85 env[KEY] = self._full_sid.encode()
86
87 # Add a version event to front of the log.
88 self._AddVersionEvent()
89
90 @property
91 def full_sid(self):
92 return self._full_sid
93
94 def _AddVersionEvent(self):
95 """Adds a 'version' event at the beginning of current log."""
96 version_event = self._CreateEventDict('version')
Ian Kasprzakfebe73f2021-01-19 09:04:28 -080097 version_event['evt'] = "2"
Ian Kasprzak30bc3542020-12-23 10:08:20 -080098 version_event['exe'] = RepoSourceVersion()
99 self._log.insert(0, version_event)
100
101 def _CreateEventDict(self, event_name):
102 """Returns a dictionary with the common keys/values for git trace2 events.
103
104 Args:
105 event_name: The event name.
106
107 Returns:
108 Dictionary with the common event fields populated.
109 """
110 return {
111 'event': event_name,
112 'sid': self._full_sid,
113 'thread': threading.currentThread().getName(),
114 'time': datetime.datetime.utcnow().isoformat() + 'Z',
115 }
116
117 def StartEvent(self):
118 """Append a 'start' event to the current log."""
119 start_event = self._CreateEventDict('start')
120 start_event['argv'] = sys.argv
121 self._log.append(start_event)
122
123 def ExitEvent(self, result):
124 """Append an 'exit' event to the current log.
125
126 Args:
127 result: Exit code of the event
128 """
129 exit_event = self._CreateEventDict('exit')
130
131 # Consider 'None' success (consistent with event_log result handling).
132 if result is None:
133 result = 0
134 exit_event['code'] = result
135 self._log.append(exit_event)
136
Raman Tennetia5b40a22021-03-16 14:24:14 -0700137 def CommandEvent(self, name, subcommands):
138 """Append a 'command' event to the current log.
139
140 Args:
141 name: Name of the primary command (ex: repo, git)
142 subcommands: List of the sub-commands (ex: version, init, sync)
143 """
144 command_event = self._CreateEventDict('command')
145 command_event['name'] = name
146 command_event['subcommands'] = subcommands
147 self._log.append(command_event)
148
Raman Tenneti7954de12021-07-28 14:36:49 -0700149 def LogConfigEvents(self, config, event_dict_name):
150 """Append a |event_dict_name| event for each config key in |config|.
151
152 Args:
153 config: Configuration dictionary.
154 event_dict_name: Name of the event dictionary for items to be logged under.
155 """
156 for param, value in config.items():
157 event = self._CreateEventDict(event_dict_name)
158 event['param'] = param
159 event['value'] = value
160 self._log.append(event)
161
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800162 def DefParamRepoEvents(self, config):
163 """Append a 'def_param' event for each repo.* config key to the current log.
164
165 Args:
166 config: Repo configuration dictionary
167 """
168 # Only output the repo.* config parameters.
169 repo_config = {k: v for k, v in config.items() if k.startswith('repo.')}
Raman Tenneti7954de12021-07-28 14:36:49 -0700170 self.LogConfigEvents(repo_config, 'def_param')
Ian Kasprzak835a34b2021-03-05 11:04:49 -0800171
Raman Tennetibbe88362021-09-27 15:15:47 -0700172 def GetDataEventName(self, value):
173 """Returns 'data-json' if the value is an array else returns 'data'."""
174 return 'data-json' if value[0] == '[' and value[-1] == ']' else 'data'
Raman Tenneti956f7362021-09-27 10:55:44 -0700175
Raman Tenneti6448a4f2021-09-13 17:40:07 -0700176 def LogDataConfigEvents(self, config, prefix):
177 """Append a 'data' event for each config key/value in |config| to the current log.
178
179 For each keyX and valueX of the config, "key" field of the event is '|prefix|/keyX'
180 and the "value" of the "key" field is valueX.
181
182 Args:
183 config: Configuration dictionary.
184 prefix: Prefix for each key that is logged.
185 """
186 for key, value in config.items():
Raman Tennetibbe88362021-09-27 15:15:47 -0700187 event = self._CreateEventDict(self.GetDataEventName(value))
Raman Tenneti6448a4f2021-09-13 17:40:07 -0700188 event['key'] = f'{prefix}/{key}'
189 event['value'] = value
190 self._log.append(event)
191
Raman Tennetifc7aa902021-06-08 17:41:08 -0700192 def ErrorEvent(self, msg, fmt):
193 """Append a 'error' event to the current log."""
194 error_event = self._CreateEventDict('error')
195 error_event['msg'] = msg
196 error_event['fmt'] = fmt
197 self._log.append(error_event)
198
Ian Kasprzak343d5852021-01-06 16:26:31 -0800199 def _GetEventTargetPath(self):
200 """Get the 'trace2.eventtarget' path from git configuration.
201
202 Returns:
203 path: git config's 'trace2.eventtarget' path if it exists, or None
204 """
205 path = None
206 cmd = ['config', '--get', 'trace2.eventtarget']
207 # TODO(https://crbug.com/gerrit/13706): Use GitConfig when it supports
208 # system git config variables.
209 p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True,
210 bare=True)
211 retval = p.Wait()
212 if retval == 0:
213 # Strip trailing carriage-return in path.
214 path = p.stdout.rstrip('\n')
215 elif retval != 1:
216 # `git config --get` is documented to produce an exit status of `1` if
217 # the requested variable is not present in the configuration. Report any
218 # other return value as an error.
219 print("repo: error: 'git config --get' call failed with return code: %r, stderr: %r" % (
220 retval, p.stderr), file=sys.stderr)
221 return path
222
Josh Steadmon244c9a72022-03-08 10:24:43 -0800223 def _WriteLog(self, write_fn):
224 """Writes the log out using a provided writer function.
225
226 Generate compact JSON output for each item in the log, and write it using
227 write_fn.
228
229 Args:
230 write_fn: A function that accepts byts and writes them to a destination.
231 """
232
233 for e in self._log:
234 # Dump in compact encoding mode.
235 # See 'Compact encoding' in Python docs:
236 # https://docs.python.org/3/library/json.html#module-json
237 write_fn(json.dumps(e, indent=None, separators=(',', ':')).encode('utf-8') + b'\n')
238
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800239 def Write(self, path=None):
Josh Steadmon244c9a72022-03-08 10:24:43 -0800240 """Writes the log out to a file or socket.
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800241
242 Log is only written if 'path' or 'git config --get trace2.eventtarget'
Josh Steadmon244c9a72022-03-08 10:24:43 -0800243 provide a valid path (or socket) to write logs to.
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800244
245 Logging filename format follows the git trace2 style of being a unique
246 (exclusive writable) file.
247
248 Args:
Josh Steadmon244c9a72022-03-08 10:24:43 -0800249 path: Path to where logs should be written. The path may have a prefix of
250 the form "af_unix:[{stream|dgram}:]", in which case the path is
251 treated as a Unix domain socket. See
252 https://git-scm.com/docs/api-trace2#_enabling_a_target for details.
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800253
254 Returns:
Josh Steadmon244c9a72022-03-08 10:24:43 -0800255 log_path: Path to the log file or socket if log is written, otherwise None
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800256 """
257 log_path = None
258 # If no logging path is specified, get the path from 'trace2.eventtarget'.
259 if path is None:
Ian Kasprzak343d5852021-01-06 16:26:31 -0800260 path = self._GetEventTargetPath()
261
262 # If no logging path is specified, exit.
263 if path is None:
264 return None
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800265
Josh Steadmon244c9a72022-03-08 10:24:43 -0800266 path_is_socket = False
267 socket_type = None
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800268 if isinstance(path, str):
Josh Steadmon244c9a72022-03-08 10:24:43 -0800269 parts = path.split(':', 1)
270 if parts[0] == 'af_unix' and len(parts) == 2:
271 path_is_socket = True
272 path = parts[1]
273 parts = path.split(':', 1)
274 if parts[0] == 'stream' and len(parts) == 2:
275 socket_type = socket.SOCK_STREAM
276 path = parts[1]
277 elif parts[0] == 'dgram' and len(parts) == 2:
278 socket_type = socket.SOCK_DGRAM
279 path = parts[1]
280 else:
281 # Get absolute path.
282 path = os.path.abspath(os.path.expanduser(path))
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800283 else:
284 raise TypeError('path: str required but got %s.' % type(path))
285
286 # Git trace2 requires a directory to write log to.
287
288 # TODO(https://crbug.com/gerrit/13706): Support file (append) mode also.
Josh Steadmon244c9a72022-03-08 10:24:43 -0800289 if not (path_is_socket or os.path.isdir(path)):
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800290 return None
Josh Steadmon244c9a72022-03-08 10:24:43 -0800291
292 if path_is_socket:
293 if socket_type == socket.SOCK_STREAM or socket_type is None:
294 try:
295 with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
296 sock.connect(path)
297 self._WriteLog(sock.sendall)
298 return f'af_unix:stream:{path}'
299 except OSError as err:
300 # If we tried to connect to a DGRAM socket using STREAM, ignore the
301 # attempt and continue to DGRAM below. Otherwise, issue a warning.
302 if err.errno != errno.EPROTOTYPE:
303 print(f'repo: warning: git trace2 logging failed: {err}', file=sys.stderr)
304 return None
305 if socket_type == socket.SOCK_DGRAM or socket_type is None:
306 try:
307 with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as sock:
308 self._WriteLog(lambda bs: sock.sendto(bs, path))
309 return f'af_unix:dgram:{path}'
310 except OSError as err:
311 print(f'repo: warning: git trace2 logging failed: {err}', file=sys.stderr)
312 return None
313 # Tried to open a socket but couldn't connect (SOCK_STREAM) or write
314 # (SOCK_DGRAM).
315 print('repo: warning: git trace2 logging failed: could not write to socket', file=sys.stderr)
316 return None
317
318 # Path is an absolute path
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800319 # Use NamedTemporaryFile to generate a unique filename as required by git trace2.
320 try:
Josh Steadmon244c9a72022-03-08 10:24:43 -0800321 with tempfile.NamedTemporaryFile(mode='xb', prefix=self._sid, dir=path,
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800322 delete=False) as f:
323 # TODO(https://crbug.com/gerrit/13706): Support writing events as they
324 # occur.
Josh Steadmon244c9a72022-03-08 10:24:43 -0800325 self._WriteLog(f.write)
Ian Kasprzak30bc3542020-12-23 10:08:20 -0800326 log_path = f.name
327 except FileExistsError as err:
328 print('repo: warning: git trace2 logging failed: %r' % err,
329 file=sys.stderr)
330 return None
331 return log_path