comparison contrib/pull_logger.py @ 49508:791050360486

contrib: add pull_logger extension This extension logs the pull parameters, i.e. the remote and common heads, when pulling from the local repository. The collected data should give an idea of the state of a pair of repositories and allow replaying past synchronisations between them. This is particularly useful for working on data exchange, bundling and caching-related optimisations.
author pacien <pacien.trangirard@pacien.net>
date Mon, 25 Jul 2022 22:47:15 +0200
parents
children 946c023212b8
comparison
equal deleted inserted replaced
49506:44bc045a43ca 49508:791050360486
1 # pull_logger.py - Logs pulls to a JSON-line file in the repo's VFS.
2 #
3 # Copyright 2022 Pacien TRAN-GIRARD <pacien.trangirard@pacien.net>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8
9 '''logs pull parameters to a file
10
11 This extension logs the pull parameters, i.e. the remote and common heads,
12 when pulling from the local repository.
13
14 The collected data should give an idea of the state of a pair of repositories
15 and allow replaying past synchronisations between them. This is particularly
16 useful for working on data exchange, bundling and caching-related
17 optimisations.
18
19 The record is a JSON-line file located in the repository's VFS at
20 .hg/pull_log.jsonl.
21
22 Log write failures are not considered fatal: log writes may be skipped for any
23 reason such as insufficient storage or a timeout.
24
25 The timeouts of the exclusive lock used when writing to the lock file can be
26 configured through the 'timeout.lock' and 'timeout.warn' options of this
27 plugin. Those are not expected to be held for a significant time in practice.::
28
29 [pull-logger]
30 timeout.lock = 300
31 timeout.warn = 100
32
33 Note: there is no automatic log rotation and the size of the log is not capped.
34 '''
35
36
37 import json
38 import time
39
40 from mercurial.i18n import _
41 from mercurial.utils import stringutil
42 from mercurial import (
43 error,
44 extensions,
45 lock,
46 registrar,
47 wireprotov1server,
48 )
49
50 EXT_NAME = b'pull-logger'
51 EXT_VERSION_CODE = 0
52
53 LOG_FILE = b'pull_log.jsonl'
54 LOCK_NAME = LOG_FILE + b'.lock'
55
56 configtable = {}
57 configitem = registrar.configitem(configtable)
58 configitem(EXT_NAME, b'timeout.lock', default=600)
59 configitem(EXT_NAME, b'timeout.warn', default=120)
60
61
62 def wrap_getbundle(orig, repo, proto, others, *args, **kwargs):
63 heads, common = extract_pull_heads(others)
64 log_entry = {
65 'timestamp': time.time(),
66 'logger_version': EXT_VERSION_CODE,
67 'heads': sorted(heads),
68 'common': sorted(common),
69 }
70
71 try:
72 write_to_log(repo, log_entry)
73 except (IOError, error.LockError) as err:
74 msg = stringutil.forcebytestr(err)
75 repo.ui.warn(_(b'unable to append to pull log: %s\n') % msg)
76
77 return orig(repo, proto, others, *args, **kwargs)
78
79
80 def extract_pull_heads(bundle_args):
81 opts = wireprotov1server.options(
82 b'getbundle',
83 wireprotov1server.wireprototypes.GETBUNDLE_ARGUMENTS.keys(),
84 bundle_args.copy(), # this call consumes the args destructively
85 )
86
87 heads = opts.get(b'heads', b'').decode('utf-8').split(' ')
88 common = opts.get(b'common', b'').decode('utf-8').split(' ')
89 return (heads, common)
90
91
92 def write_to_log(repo, entry):
93 locktimeout = repo.ui.configint(EXT_NAME, b'timeout.lock')
94 lockwarntimeout = repo.ui.configint(EXT_NAME, b'timeout.warn')
95
96 with lock.trylock(
97 ui=repo.ui,
98 vfs=repo.vfs,
99 lockname=LOCK_NAME,
100 timeout=locktimeout,
101 warntimeout=lockwarntimeout,
102 ):
103 with repo.vfs.open(LOG_FILE, b'a+') as logfile:
104 serialised = json.dumps(entry, sort_keys=True)
105 logfile.write(serialised.encode('utf-8'))
106 logfile.write(b'\n')
107 logfile.flush()
108
109
110 def reposetup(ui, repo):
111 if repo.local():
112 repo._wlockfreeprefix.add(LOG_FILE)
113
114
115 def uisetup(ui):
116 del wireprotov1server.commands[b'getbundle']
117 decorator = wireprotov1server.wireprotocommand(
118 name=b'getbundle',
119 args=b'*',
120 permission=b'pull',
121 )
122
123 extensions.wrapfunction(
124 container=wireprotov1server,
125 funcname='getbundle',
126 wrapper=wrap_getbundle,
127 )
128
129 decorator(wireprotov1server.getbundle)