Package: callmonitor
callmonitor-- A Simple Tool to Monitor and Log Function CallsInstallationpipinstallcallmonitoror clone this repo and:pythonsetup.pyinstallUsageIt's simple to use, just decorate any function with the@interceptdecorator.
Eg:fromcallmonitorimportintercept@interceptdeftest_fn_2(x,y=2,z=3):passThis will save the inputs (args,kwargsandargspec) along with a call
database (callmonitor.DB) to:call-monitor/test_fn_2/<invocation count>.callmonitorDoesn't Overwrite OutputIf thecall-monitorfolder already exists (eg. a previous run), then a new
foldercall-monitor-1, orcall-monitor-2, and so on, is created. See the
sections onData Structurefor more details onhowthis data is saved.Multi-Threading/Process SafeTo avoid different processes from writing to the same location,callmonitorappends-tid=<N>to the root (call-monitor) folder. Currentlycallmonitorsupportsmpi4pyout of the box: ifmpi4py.MPI.COMM_WORLD.Get_rank() > 1,callmonitorautomatically assumes that it's running im multi-threaded mode
and appends-tid=<Get_rank()>to the output. If your programm is
multi-threaded with another framwork (eg.concurrent.Futures) then you need
to tellcallmonitoryour thread ID usingcallmonitor.Settings:fromcallmonitorimportSettingssettings=Settings()settings.enable_multi_threading(THREAD_ID)beforethe first invocation ofintercept(the database is created on disk
when it is first needed, it is at that point whencallmonitor.Settingsis
read. Any changes made tocallmonitor.Settingsafterwards will only take
effect if the database is recreated -- usingcallmonitor.CONTEXT.new).Registering your own ArgumentHandlersSometimespicklejust won't cut it in terms of saving function inputs -- eg.
when we need to save our own fancy data types.callmonitorprovides a way of
building your down argument handlers and registering to the globalcallmonitor.REGISTRY. The registry is queried every time function inputs are
processed, so if you build your ownArgHandlerand add them usinggcallmonitor.REGISTRY.add, it will process any arguments of the associated
datatype from that point forward. Eg,numpyprovides its ownsave/loadfunctions. We have already build (and registered) a numpy arggument handler
like so:importnumpyasnpfromos.pathimportjoinfromcallmonitorimportHandler,REGISTRYclassNPHandler(Handler):defload(self,path):self.data=np.load(join(path,f"arg_{self.target}.npy"))defsave(self,path):np.save(join(path,f"arg_{self.target}.npy"),self.data)@classmethoddefaccumulator_done(cls):passREGISTRY.add(np.ndarray,NPHandler)(remember thatcallmonitor.REGISTRY.addneeds to be calledbeforethe
first invocation of@interceptthat needs this particularHandler). A
custom handler needs to inherit thecallmonitor.Handlerclass and definesave,load, andaccumulator_done(the last one being a@classmethod).Loading Datacallmonitor.load(<path>)will load a database at<path>(see section onData Structure). Eg:fromcallmonitorimportloaddb=load("call-monitor")We can now get individual function calls data from the database usingDB.get:args,kwargs=db.get("function_name",invocation_count)(which will also automatically load.npyfiles and any custom handlers --
remember to register these incallmonitor.REGISTRYbefore executingdb.get)Remember:invocation_countstarts at 1. Therefore to access thefirstcall totest_np_1, run:In[4]:db.get("test_np_1",1)Out[4]:([10,array([0.,0.,0.,0.,0.,0.,0.,0.,0.,0.])],{})Interacting withcallmonitorWe try to enable top-level summaries of the following user-facing classes:REGISTRYDBDB.get_args, andArgsvia the__str__and__repr__functions. Eg,callmonitor.REGISTRYshows
which datatype/handler pairs are configured:In[2]:callmonitor.REGISTRYOut[2]:{<class'numpy.ndarray'>: <class 'callmonitor.handler.NPHandler'>}Likewise theDBobject displays a summary of functions called and how often.In[3]:db=callmonitor.load("call-monitor")In[4]:dbOut[4]:{Locked:Truetest_np_1:{calls:2args:['x','n']defaults:None}}ArgsContainerPicking apartargs,kwargs, andargspec.defaultscan be very tedious --
especially if you're trying to find out the value of a specific argument. Hencecallmonitor.DBprovides an additionl getter --get_argswhich returns anArgsobject.callmonitor.Argsare container classes that store each input
argument by name as an attributed. Eg:In[3]:args=db.get_args("test_fn_1",1)In[4]:argsOut[4]:dict_keys(['x','y','z'])In[5]:args.xOut[5]:1Note: thecallmonitor.Argsconstructor will fill in any arguments not inargsandkwargsfrom theFullArgSpecdefaults. If you just want to
recreate the original function call theargsandkwargsreturned bycallmonitor.DB.getshould be enough.Data StructureWhile not technically adatabase, let's call the directories generated bycallmonitora database for the lack of a better term. Each database consists
of adb.pklfile (containing metadata), as well as folders for each function
(each function call is enumerated). Eg:call-monitor
├── db.pkl
├── test_fn_1
│   ├── 1
│   │   └── input_descriptor.pkl
│   └── 2
│       └── input_descriptor.pkl
└── test_fn_2
    └── 1
        └── input_descriptor.pklSpecial attention is given tonumpyinputs -- these are calledarg_<label>.npy, where<label>is either the index of the input argument,
or thekwfor kwargs. Eg:call-monitor
├── db.pkl
└── test_np_1
    ├── 1
    │   ├── arg_1.npy
    │   └── input_descriptor.pkl
    └── 2
        ├── arg_n.npy
        └── input_descriptor.pklFull consideration was given to savingallcall data in a single data
structure -- maybe even arealdatabase ;) -- but to do this efficiently at
scale is not easy, and might make this package cumbersome. Future versions will
include the ability to fuse multiple small function calls into a single
accumulator object to avoid large numbers of small files.Backward CompatibilityVersion 0.3.0 brigns many enhancements tocallmonitor. We therefore could no
longer enable native backward compatibility. A tool that can convert an version
0.2.0 database to a version 0.3.0 (or later) is currently being developed.
Versions pre-dating 0.2.0 are no longer supported.