contrib/pull_logger.py
changeset 49508 791050360486
child 49509 946c023212b8
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/pull_logger.py	Mon Jul 25 22:47:15 2022 +0200
@@ -0,0 +1,129 @@
+# pull_logger.py - Logs pulls to a JSON-line file in the repo's VFS.
+#
+# Copyright 2022  Pacien TRAN-GIRARD <pacien.trangirard@pacien.net>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+
+'''logs pull parameters to a file
+
+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.
+
+The record is a JSON-line file located in the repository's VFS at
+.hg/pull_log.jsonl.
+
+Log write failures are not considered fatal: log writes may be skipped for any
+reason such as insufficient storage or a timeout.
+
+The timeouts of the exclusive lock used when writing to the lock file can be
+configured through the 'timeout.lock' and 'timeout.warn' options of this
+plugin. Those are not expected to be held for a significant time in practice.::
+
+  [pull-logger]
+  timeout.lock = 300
+  timeout.warn = 100
+
+Note: there is no automatic log rotation and the size of the log is not capped.
+'''
+
+
+import json
+import time
+
+from mercurial.i18n import _
+from mercurial.utils import stringutil
+from mercurial import (
+    error,
+    extensions,
+    lock,
+    registrar,
+    wireprotov1server,
+)
+
+EXT_NAME = b'pull-logger'
+EXT_VERSION_CODE = 0
+
+LOG_FILE = b'pull_log.jsonl'
+LOCK_NAME = LOG_FILE + b'.lock'
+
+configtable = {}
+configitem = registrar.configitem(configtable)
+configitem(EXT_NAME, b'timeout.lock', default=600)
+configitem(EXT_NAME, b'timeout.warn', default=120)
+
+
+def wrap_getbundle(orig, repo, proto, others, *args, **kwargs):
+    heads, common = extract_pull_heads(others)
+    log_entry = {
+        'timestamp': time.time(),
+        'logger_version': EXT_VERSION_CODE,
+        'heads': sorted(heads),
+        'common': sorted(common),
+    }
+
+    try:
+        write_to_log(repo, log_entry)
+    except (IOError, error.LockError) as err:
+        msg = stringutil.forcebytestr(err)
+        repo.ui.warn(_(b'unable to append to pull log: %s\n') % msg)
+
+    return orig(repo, proto, others, *args, **kwargs)
+
+
+def extract_pull_heads(bundle_args):
+    opts = wireprotov1server.options(
+        b'getbundle',
+        wireprotov1server.wireprototypes.GETBUNDLE_ARGUMENTS.keys(),
+        bundle_args.copy(),  # this call consumes the args destructively
+    )
+
+    heads = opts.get(b'heads', b'').decode('utf-8').split(' ')
+    common = opts.get(b'common', b'').decode('utf-8').split(' ')
+    return (heads, common)
+
+
+def write_to_log(repo, entry):
+    locktimeout = repo.ui.configint(EXT_NAME, b'timeout.lock')
+    lockwarntimeout = repo.ui.configint(EXT_NAME, b'timeout.warn')
+
+    with lock.trylock(
+        ui=repo.ui,
+        vfs=repo.vfs,
+        lockname=LOCK_NAME,
+        timeout=locktimeout,
+        warntimeout=lockwarntimeout,
+    ):
+        with repo.vfs.open(LOG_FILE, b'a+') as logfile:
+            serialised = json.dumps(entry, sort_keys=True)
+            logfile.write(serialised.encode('utf-8'))
+            logfile.write(b'\n')
+            logfile.flush()
+
+
+def reposetup(ui, repo):
+    if repo.local():
+        repo._wlockfreeprefix.add(LOG_FILE)
+
+
+def uisetup(ui):
+    del wireprotov1server.commands[b'getbundle']
+    decorator = wireprotov1server.wireprotocommand(
+        name=b'getbundle',
+        args=b'*',
+        permission=b'pull',
+    )
+
+    extensions.wrapfunction(
+        container=wireprotov1server,
+        funcname='getbundle',
+        wrapper=wrap_getbundle,
+    )
+
+    decorator(wireprotov1server.getbundle)