summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIan Kasprzak <iankaz@google.com>2020-12-23 10:08:20 -0800
committerIan Kasprzak <iankaz@google.com>2021-01-06 17:49:41 +0000
commit30bc354e25cb5ccfbbb276a93ec54195e6b966a4 (patch)
treebf4e82ca88ade5a57b2fb1e46a6a8991c84f7b98
parentce9b6c43b2394f3f8c9413abfa3a10597de54223 (diff)
downloadgit-repo-30bc354e25cb5ccfbbb276a93ec54195e6b966a4.tar.gz
Enable git trace2 event format logging.
Ways to enable logging: 1) Set git's trace2.eventtarget config variable to desired logging path 2) Specify path via --git_trace2_event_log option A unique logfile name is generated per repo execution (based on the repo session-id). Testing: 1) Verified git config 'trace2.eventtarget' and flag enable logging. 2) Verified version/start/end events are expected format: https://git-scm.com/docs/api-trace2#_event_format 3) Unit tests Bug: https://crbug.com/gerrit/13706 Change-Id: I335eba68124055321c4149979bec36ac16ef81eb Tested-by: Ian Kasprzak <iankaz@google.com> Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/292262 Reviewed-by: Mike Frysinger <vapier@google.com> Reviewed-by: Jonathan Nieder <jrn@google.com>
-rw-r--r--git_trace2_event_log.py197
-rwxr-xr-xmain.py9
-rw-r--r--tests/test_git_trace2_event_log.py169
3 files changed, 375 insertions, 0 deletions
diff --git a/git_trace2_event_log.py b/git_trace2_event_log.py
new file mode 100644
index 00000000..4a8e0347
--- /dev/null
+++ b/git_trace2_event_log.py
@@ -0,0 +1,197 @@
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
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')
95 version_event['evt'] = 2
96 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
135 def Write(self, path=None):
136 """Writes the log out to a file.
137
138 Log is only written if 'path' or 'git config --get trace2.eventtarget'
139 provide a valid path to write logs to.
140
141 Logging filename format follows the git trace2 style of being a unique
142 (exclusive writable) file.
143
144 Args:
145 path: Path to where logs should be written.
146
147 Returns:
148 log_path: Path to the log file if log is written, otherwise None
149 """
150 log_path = None
151 # If no logging path is specified, get the path from 'trace2.eventtarget'.
152 if path is None:
153 cmd = ['config', '--get', 'trace2.eventtarget']
154 # TODO(https://crbug.com/gerrit/13706): Use GitConfig when it supports
155 # system git config variables.
156 p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True,
157 bare=True)
158 retval = p.Wait()
159 if retval == 0:
160 # Strip trailing carriage-return in path.
161 path = p.stdout.rstrip('\n')
162 elif retval != 1:
163 # `git config --get` is documented to produce an exit status of `1` if
164 # the requested variable is not present in the configuration. Report any
165 # other return value as an error.
166 print("repo: error: 'git config --get' call failed with return code: %r, stderr: %r" % (
167 retval, p.stderr), file=sys.stderr)
168
169 if isinstance(path, str):
170 # Get absolute path.
171 path = os.path.abspath(os.path.expanduser(path))
172 else:
173 raise TypeError('path: str required but got %s.' % type(path))
174
175 # Git trace2 requires a directory to write log to.
176
177 # TODO(https://crbug.com/gerrit/13706): Support file (append) mode also.
178 if not os.path.isdir(path):
179 return None
180 # Use NamedTemporaryFile to generate a unique filename as required by git trace2.
181 try:
182 with tempfile.NamedTemporaryFile(mode='x', prefix=self._sid, dir=path,
183 delete=False) as f:
184 # TODO(https://crbug.com/gerrit/13706): Support writing events as they
185 # occur.
186 for e in self._log:
187 # Dump in compact encoding mode.
188 # See 'Compact encoding' in Python docs:
189 # https://docs.python.org/3/library/json.html#module-json
190 json.dump(e, f, indent=None, separators=(',', ':'))
191 f.write('\n')
192 log_path = f.name
193 except FileExistsError as err:
194 print('repo: warning: git trace2 logging failed: %r' % err,
195 file=sys.stderr)
196 return None
197 return log_path
diff --git a/main.py b/main.py
index e152de4f..a7c3c684 100755
--- a/main.py
+++ b/main.py
@@ -50,6 +50,7 @@ import event_log
50from repo_trace import SetTrace 50from repo_trace import SetTrace
51from git_command import user_agent 51from git_command import user_agent
52from git_config import init_ssh, close_ssh, RepoConfig 52from git_config import init_ssh, close_ssh, RepoConfig
53from git_trace2_event_log import EventLog
53from command import InteractiveCommand 54from command import InteractiveCommand
54from command import MirrorSafeCommand 55from command import MirrorSafeCommand
55from command import GitcAvailableCommand, GitcClientCommand 56from command import GitcAvailableCommand, GitcClientCommand
@@ -130,6 +131,8 @@ global_options.add_option('--version',
130global_options.add_option('--event-log', 131global_options.add_option('--event-log',
131 dest='event_log', action='store', 132 dest='event_log', action='store',
132 help='filename of event log to append timeline to') 133 help='filename of event log to append timeline to')
134global_options.add_option('--git-trace2-event-log', action='store',
135 help='directory to write git trace2 event log to')
133 136
134 137
135class _Repo(object): 138class _Repo(object):
@@ -211,6 +214,7 @@ class _Repo(object):
211 file=sys.stderr) 214 file=sys.stderr)
212 return 1 215 return 1
213 216
217 git_trace2_event_log = EventLog()
214 cmd.repodir = self.repodir 218 cmd.repodir = self.repodir
215 cmd.client = RepoClient(cmd.repodir) 219 cmd.client = RepoClient(cmd.repodir)
216 cmd.manifest = cmd.client.manifest 220 cmd.manifest = cmd.client.manifest
@@ -261,6 +265,8 @@ class _Repo(object):
261 start = time.time() 265 start = time.time()
262 cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start) 266 cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
263 cmd.event_log.SetParent(cmd_event) 267 cmd.event_log.SetParent(cmd_event)
268 git_trace2_event_log.StartEvent()
269
264 try: 270 try:
265 cmd.ValidateOptions(copts, cargs) 271 cmd.ValidateOptions(copts, cargs)
266 result = cmd.Execute(copts, cargs) 272 result = cmd.Execute(copts, cargs)
@@ -303,10 +309,13 @@ class _Repo(object):
303 309
304 cmd.event_log.FinishEvent(cmd_event, finish, 310 cmd.event_log.FinishEvent(cmd_event, finish,
305 result is None or result == 0) 311 result is None or result == 0)
312 git_trace2_event_log.ExitEvent(result)
313
306 if gopts.event_log: 314 if gopts.event_log:
307 cmd.event_log.Write(os.path.abspath( 315 cmd.event_log.Write(os.path.abspath(
308 os.path.expanduser(gopts.event_log))) 316 os.path.expanduser(gopts.event_log)))
309 317
318 git_trace2_event_log.Write(gopts.git_trace2_event_log)
310 return result 319 return result
311 320
312 321
diff --git a/tests/test_git_trace2_event_log.py b/tests/test_git_trace2_event_log.py
new file mode 100644
index 00000000..3905630f
--- /dev/null
+++ b/tests/test_git_trace2_event_log.py
@@ -0,0 +1,169 @@
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
15"""Unittests for the git_trace2_event_log.py module."""
16
17import json
18import tempfile
19import unittest
20
21import git_trace2_event_log
22
23
24class EventLogTestCase(unittest.TestCase):
25 """TestCase for the EventLog module."""
26
27 PARENT_SID_KEY = 'GIT_TRACE2_PARENT_SID'
28 PARENT_SID_VALUE = 'parent_sid'
29 SELF_SID_REGEX = r'repo-\d+T\d+Z-.*'
30 FULL_SID_REGEX = r'^%s/%s' % (PARENT_SID_VALUE, SELF_SID_REGEX)
31
32 def setUp(self):
33 """Load the event_log module every time."""
34 self._event_log_module = None
35 # By default we initialize with the expected case where
36 # repo launches us (so GIT_TRACE2_PARENT_SID is set).
37 env = {
38 self.PARENT_SID_KEY: self.PARENT_SID_VALUE,
39 }
40 self._event_log_module = git_trace2_event_log.EventLog(env=env)
41 self._log_data = None
42
43 def verifyCommonKeys(self, log_entry, expected_event_name, full_sid=True):
44 """Helper function to verify common event log keys."""
45 self.assertIn('event', log_entry)
46 self.assertIn('sid', log_entry)
47 self.assertIn('thread', log_entry)
48 self.assertIn('time', log_entry)
49
50 # Do basic data format validation.
51 self.assertEqual(expected_event_name, log_entry['event'])
52 if full_sid:
53 self.assertRegex(log_entry['sid'], self.FULL_SID_REGEX)
54 else:
55 self.assertRegex(log_entry['sid'], self.SELF_SID_REGEX)
56 self.assertRegex(log_entry['time'], r'^\d+-\d+-\d+T\d+:\d+:\d+\.\d+Z$')
57
58 def readLog(self, log_path):
59 """Helper function to read log data into a list."""
60 log_data = []
61 with open(log_path, mode='rb') as f:
62 for line in f:
63 log_data.append(json.loads(line))
64 return log_data
65
66 def test_initial_state_with_parent_sid(self):
67 """Test initial state when 'GIT_TRACE2_PARENT_SID' is set by parent."""
68 self.assertRegex(self._event_log_module.full_sid, self.FULL_SID_REGEX)
69
70 def test_initial_state_no_parent_sid(self):
71 """Test initial state when 'GIT_TRACE2_PARENT_SID' is not set."""
72 # Setup an empty environment dict (no parent sid).
73 self._event_log_module = git_trace2_event_log.EventLog(env={})
74 self.assertRegex(self._event_log_module.full_sid, self.SELF_SID_REGEX)
75
76 def test_version_event(self):
77 """Test 'version' event data is valid.
78
79 Verify that the 'version' event is written even when no other
80 events are addded.
81
82 Expected event log:
83 <version event>
84 """
85 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
86 log_path = self._event_log_module.Write(path=tempdir)
87 self._log_data = self.readLog(log_path)
88
89 # A log with no added events should only have the version entry.
90 self.assertEqual(len(self._log_data), 1)
91 version_event = self._log_data[0]
92 self.verifyCommonKeys(version_event, expected_event_name='version')
93 # Check for 'version' event specific fields.
94 self.assertIn('evt', version_event)
95 self.assertIn('exe', version_event)
96
97 def test_start_event(self):
98 """Test and validate 'start' event data is valid.
99
100 Expected event log:
101 <version event>
102 <start event>
103 """
104 self._event_log_module.StartEvent()
105 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
106 log_path = self._event_log_module.Write(path=tempdir)
107 self._log_data = self.readLog(log_path)
108
109 self.assertEqual(len(self._log_data), 2)
110 start_event = self._log_data[1]
111 self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
112 self.verifyCommonKeys(start_event, expected_event_name='start')
113 # Check for 'start' event specific fields.
114 self.assertIn('argv', start_event)
115 self.assertTrue(isinstance(start_event['argv'], list))
116
117 def test_exit_event_result_none(self):
118 """Test 'exit' event data is valid when result is None.
119
120 We expect None result to be converted to 0 in the exit event data.
121
122 Expected event log:
123 <version event>
124 <exit event>
125 """
126 self._event_log_module.ExitEvent(None)
127 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
128 log_path = self._event_log_module.Write(path=tempdir)
129 self._log_data = self.readLog(log_path)
130
131 self.assertEqual(len(self._log_data), 2)
132 exit_event = self._log_data[1]
133 self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
134 self.verifyCommonKeys(exit_event, expected_event_name='exit')
135 # Check for 'exit' event specific fields.
136 self.assertIn('code', exit_event)
137 # 'None' result should convert to 0 (successful) return code.
138 self.assertEqual(exit_event['code'], 0)
139
140 def test_exit_event_result_integer(self):
141 """Test 'exit' event data is valid when result is an integer.
142
143 Expected event log:
144 <version event>
145 <exit event>
146 """
147 self._event_log_module.ExitEvent(2)
148 with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
149 log_path = self._event_log_module.Write(path=tempdir)
150 self._log_data = self.readLog(log_path)
151
152 self.assertEqual(len(self._log_data), 2)
153 exit_event = self._log_data[1]
154 self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
155 self.verifyCommonKeys(exit_event, expected_event_name='exit')
156 # Check for 'exit' event specific fields.
157 self.assertIn('code', exit_event)
158 self.assertEqual(exit_event['code'], 2)
159
160 # TODO(https://crbug.com/gerrit/13706): Add additional test coverage for
161 # Write() where:
162 # - path=None (using git config call)
163 # - path=<Non-String type> (raises TypeError)
164 # - path=<Non-Directory> (should return None)
165 # - tempfile.NamedTemporaryFile errors with FileExistsError (should return None)
166
167
168if __name__ == '__main__':
169 unittest.main()