探針的實現主要涉及以下幾個知識點:
sys.meta_path
sitecustomize.py
sys.meta_path
sys.meta_path這個簡單的來說就是可以實現 import hook 的功能, 當執行 import 相關的操作時,會觸發 sys.meta_path 列表中定義的對象。關於 sys.meta_path 更詳細的資料請查閱 python 文檔中 sys.meta_path 相關內容以及 PEP 0302 。
sys.meta_path 中的對象需要實現一個 find_module 方法, 這個 find_module 方法返回 None 或一個實現了 load_module 方法的對象 (代碼可以從 github 上下載 part1) :
import sysclass MetaPathFinder: def find_module(self, fullname, path=None): print('find_module {}'.format(fullname)) return MetaPathLoader()class MetaPathLoader: def load_module(self, fullname): print('load_module {}'.format(fullname)) sys.modules[fullname] = sys return syssys.meta_path.insert(0, MetaPathFinder())if __name__ == '__main__': import http print(http) print(http.version_info)load_module 方法返回一個 module 對象,這個對象就是 import 的 module 對象了。比如我上面那樣就把 http 替換為 sys 這個 module 了。
$ python meta_path1.pyfind_module httpload_module http<module 'sys' (built-in)>sys.version_info(major=3, minor=5, micro=1, releaselevel='final', serial=0)通過 sys.meta_path 我們就可以實現 import hook 的功能:當 import 預定的 module 時,對這個 module 裡的對象來個狸貓換太子, 從而實現獲取函數或方法的執行時間等探測信息。
上面說到了狸貓換太子,那麼怎麼對一個對象進行狸貓換太子的操作呢?對於函數對象,我們可以使用裝飾器的方式來替換函數對象(代碼可以從 github 上下載 part2) :
import functoolsimport timedef func_wrapper(func): @functools.wraps(func) def wrapper(*args, **kwargs): print('start func') start = time.time() result = func(*args, **kwargs) end = time.time() print('spent {}s'.format(end - start)) return result return wrapperdef sleep(n): time.sleep(n) return nif __name__ == '__main__':func = func_wrapper(sleep) print(func(3))我們的 import hook 是 hook.py:
import functoolsimport importlibimport sysimport time_hook_modules = {'hello'}class MetaPathFinder:def find_module(self, fullname, path=None): print('find_module {}'.format(fullname)) if fullname in _hook_modules: return MetaPathLoader()class MetaPathLoader:def load_module(self, fullname): print('load_module {}'.format(fullname))# ``sys.modules`` 中保存的是已經導入過的 module if fullname in sys.modules: return sys.modules[fullname]# 先從 sys.meta_path 中刪除自定義的 finder# 防止下面執行 import_module 的時候再次觸發此 finder# 從而出現遞歸調用的問題 finder = sys.meta_path.pop(0)# 導入 module module = importlib.import_module(fullname) module_hook(fullname, module) sys.meta_path.insert(0, finder) return modulesys.meta_path.insert(0, MetaPathFinder())def module_hook(fullname, module): if fullname == 'hello': module.sleep = func_wrapper(module.sleep)def func_wrapper(func): @functools.wraps(func)def wrapper(*args, **kwargs): print('start func') start = time.time() result = func(*args, **kwargs) end = time.time() print('spent {}s'.format(end - start)) return result return wrapper
實現一個agent:
import osimport syscurrent_dir = os.path.dirname(os.path.realpath(__file__))boot_dir = os.path.join(current_dir, 'bootstrap')def main(): args = sys.argv[1:] os.environ['PYTHONPATH'] = boot_dir# 執行後面的 python 程序命令# sys.executable 是 python 解釋器程序的絕對路徑 ``which python``# >>> sys.executable# '/usr/local/var/pyenv/versions/3.5.1/bin/python3.5' os.execl(sys.executable, sys.executable, *args)if __name__ == '__main__': main()