pull_logger.py
141 lines
| 4.2 KiB
| text/x-python
|
PythonLexer
/ contrib / pull_logger.py
pacien
|
r50403 | # 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. | ||||
pacien
|
r50404 | Some basic log file rotation can be enabled by setting 'rotate-size' to a value | ||
greater than 0. This causes the current log file to be moved to | ||||
.hg/pull_log.jsonl.rotated when this threshold is met, discarding any | ||||
previously rotated log file. | ||||
pacien
|
r50403 | 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 | ||||
pacien
|
r50404 | rotate-size = 1kb | ||
pacien
|
r50403 | ''' | ||
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' | ||||
pacien
|
r50404 | OLD_LOG_FILE = LOG_FILE + b'.rotated' | ||
pacien
|
r50403 | 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) | ||||
pacien
|
r50404 | configitem(EXT_NAME, b'rotate-size', default=b'100MB') | ||
pacien
|
r50403 | |||
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') | ||||
pacien
|
r50404 | rotatesize = repo.ui.configbytes(EXT_NAME, b'rotate-size') | ||
pacien
|
r50403 | |||
with lock.trylock( | ||||
ui=repo.ui, | ||||
vfs=repo.vfs, | ||||
lockname=LOCK_NAME, | ||||
timeout=locktimeout, | ||||
warntimeout=lockwarntimeout, | ||||
): | ||||
pacien
|
r50404 | if rotatesize > 0 and repo.vfs.exists(LOG_FILE): | ||
if repo.vfs.stat(LOG_FILE).st_size >= rotatesize: | ||||
repo.vfs.rename(LOG_FILE, OLD_LOG_FILE) | ||||
pacien
|
r50403 | 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) | ||||
pacien
|
r50404 | repo._wlockfreeprefix.add(OLD_LOG_FILE) | ||
pacien
|
r50403 | |||
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) | ||||