{"id":26163505,"url":"https://github.com/jlevy/funlog","last_synced_at":"2025-10-06T10:15:48.429Z","repository":{"id":279906175,"uuid":"940294224","full_name":"jlevy/funlog","owner":"jlevy","description":"Simple, useful decorators for logging, timing, and tallying function calls","archived":false,"fork":false,"pushed_at":"2025-02-28T05:15:29.000Z","size":30,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-28T12:42:49.739Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jlevy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-02-27T23:43:25.000Z","updated_at":"2025-02-28T05:15:32.000Z","dependencies_parsed_at":"2025-02-28T12:43:03.201Z","dependency_job_id":"98f20653-610f-4f48-bb84-1fb885e6da58","html_url":"https://github.com/jlevy/funlog","commit_stats":null,"previous_names":["jlevy/funlog"],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jlevy%2Ffunlog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jlevy%2Ffunlog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jlevy%2Ffunlog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jlevy%2Ffunlog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jlevy","download_url":"https://codeload.github.com/jlevy/funlog/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243050938,"owners_count":20228101,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-11T14:27:23.760Z","updated_at":"2025-10-06T10:15:48.411Z","avatar_url":"https://github.com/jlevy.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# funlog\n\n`funlog` is a tiny but useful package that offers a few Python decorators to log or\ntally function calls, with good control over what gets logged and when.\n\n## Why Decorator Logging?\n\nWe all do quick print debugging sometimes.\nSometimes this is via log statements or other times simply with `print()`.\n\nLogging decorators are a nice compromise between the simplicity of print debugging with\nmore complex or careful log statements:\n\n```python\n@log_calls()\ndef add(a, b):\n    return a + b\n```\n\nThen in the logs you will have:\n```\nINFO:≫ Call: __main__.add(5, 5)\nINFO:≪ Call done: __main__.add() took 0.00ms: 10\n```\n\nIn addition to logging function calls, `funlog` decorators can log arguments briefly but\nclearly, abbreviating arguments like long strings or dataclasses and also time the\nfunction call (and logs it in a friendly way in milliseconds or seconds).\n\nIt's not always a good idea to do logging this way, but sometimes it saves time or\ntyping compared to an equivalent manual log statement (see the [mini FAQ](#mini-faq)).\n\nIn addition to logging calls, it lets you do *very* simple profiling by having warnings\nin production when certain functions are take more than a specified amount of time.\nFinally, you can use the decorators to get tallies of function calls and runtimes per\nfunction after a program runs a while or at exit.\n\nIt deliberately has **zero dependencies** and is a single file with ~500 lines of code.\n\nI'm publishing it standalone since I have found over the years I frequently want to drop\nit into projects, even if only for faster debugging.\n\n## Installation\n\nAdd the [`funlog`](https://pypi.org/project/funlog/) package to your environment in the\nusual way with `pip install funlog`, `poetry add funlog`, or `uv add funlog`.\n\nOr if for some reason you prefer not to change the dependencies of your project at all,\njust copy the single file [`funlog.py`](/src/funlog/funlog.py).\n\n## Examples\n\nA more realistic example is the small\n[lint script](https://github.com/jlevy/funlog/blob/main/devtools/lint.py) from this\nproject:\n\n```python\nimport subprocess\nfrom rich import print as rprint\nfrom funlog import log_calls\n\n@log_calls(level=\"warning\", show_timing_only=True)\ndef run(cmd: list[str]) -\u003e int:\n    rprint()\n    rprint(f\"[bold green]❯ {' '.join(cmd)}[/bold green]\")\n    errcount = 0\n    try:\n        subprocess.run(cmd, text=True, check=True)\n    except subprocess.CalledProcessError as e:\n        rprint(f\"[bold red]Error: {e}[/bold red]\")\n        errcount = 1\n\n    return errcount\n```\n\nIt has the output\n\n```\n❯ codespell --write-changes src tests devtools README.md\n⏱ Call to run took 88.78ms\n\n❯ ruff check --fix src tests devtools\nAll checks passed!\n⏱ Call to run took 310ms\n\n❯ ruff format src tests devtools\n4 files left unchanged\n⏱ Call to run took 10.81ms\n\n❯ basedpyright src tests devtools\n0 errors, 0 warnings, 0 notes\n⏱ Call to run took 2.07s\n```\n\nHere is a contrived example to illustrate recursive calls and tallies:\n\n```python\nimport time\nimport logging\nfrom funlog import log_calls, log_tallies, tally_calls\n\n# Set up logging however you like.\nlogging.basicConfig(level=logging.DEBUG, format=\"%(levelname)s:%(message)s\", force=True)\n\n\n@log_calls()\ndef add(a, b):\n    return a + b\n\n\n@tally_calls()\ndef sleep(n):\n    time.sleep(0.01 * n)\n\n\n@tally_calls()\ndef fibonacci(n):\n    if n \u003c= 1:\n        return n\n    sleep(n)\n    return fibonacci(n - 1) + fibonacci(n - 2)\n\n\n@log_calls()\ndef long_range(n):\n    time.sleep(0.01 * n)\n    return \" \".join(str(i) for i in range(int(n)))\n\n\n# Now call the functions:\nlong_range(fibonacci(add(add(5, 5), 2)))\n\n# And then log tallies of all calls:\nlog_tallies()\n```\n\nRunning that gives you:\n\n```\nINFO:≫ Call: __main__.add(5, 5)\nINFO:≪ Call done: __main__.add() took 0.00ms: 10\nINFO:≫ Call: __main__.add(10, 2)\nINFO:≪ Call done: __main__.add() took 0.00ms: 12\nINFO:⏱ __main__.sleep() took 125ms, now called 1 times, 125ms avg per call, total time 125ms\nINFO:⏱ __main__.sleep() took 114ms, now called 2 times, 119ms avg per call, total time 239ms\nINFO:⏱ __main__.sleep() took 95.03ms, now called 4 times, 109ms avg per call, total time 438ms\nINFO:⏱ __main__.sleep() took 55.05ms, now called 8 times, 89.25ms avg per call, total time 714ms\nINFO:⏱ __main__.fibonacci() took 0.00ms, now called 1 times, 0.00ms avg per call, total time 0.00ms\nINFO:⏱ __main__.fibonacci() took 0.00ms, now called 2 times, 0.00ms avg per call, total time 0.00ms\nINFO:⏱ __main__.fibonacci() took 25.22ms, now called 3 times, 8.41ms avg per call, total time 25.22ms\nINFO:⏱ __main__.fibonacci() took 59.16ms, now called 5 times, 16.88ms avg per call, total time 84.38ms\nINFO:⏱ __main__.fibonacci() took 128ms, now called 9 times, 26.41ms avg per call, total time 238ms\nINFO:⏱ __main__.fibonacci() took 243ms, now called 15 times, 37.59ms avg per call, total time 564ms\nINFO:⏱ __main__.sleep() took 33.76ms, now called 16 times, 60.92ms avg per call, total time 975ms\nINFO:⏱ __main__.fibonacci() took 429ms, now called 25 times, 49.00ms avg per call, total time 1.23s\nINFO:⏱ __main__.fibonacci() took 741ms, now called 41 times, 61.40ms avg per call, total time 2.52s\nINFO:⏱ __main__.sleep() took 32.68ms, now called 32 times, 48.04ms avg per call, total time 1.54s\nINFO:⏱ __main__.fibonacci() took 23.35ms, now called 75 times, 67.37ms avg per call, total time 5.05s\nINFO:⏱ __main__.sleep() took 24.54ms, now called 64 times, 43.64ms avg per call, total time 2.79s\nINFO:⏱ __main__.fibonacci() took 60.07ms, now called 129 times, 78.67ms avg per call, total time 10.15s\nINFO:⏱ __main__.fibonacci() took 55.71ms, now called 223 times, 91.26ms avg per call, total time 20.35s\nINFO:⏱ __main__.sleep() took 44.42ms, now called 128 times, 40.64ms avg per call, total time 5.20s\nINFO:⏱ __main__.fibonacci() took 2.07s, now called 396 times, 107ms avg per call, total time 42.19s\nINFO:≫ Call: __main__.long_range(144)\nINFO:≪ Call done: __main__.long_range() took 1.45s: '0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 …' (465 chars)\nINFO:⏱ Function tallies:\n    __main__.fibonacci() was called 465 times, total time 59.73s, avg per call 128ms\n    __main__.sleep() was called 232 times, total time 9.11s, avg per call 39.25ms\n```\n\nThere are several other options.\nSee docstrings and [test_examples.py](tests/test_examples.py) for more docs and examples\non all the options.\n\n## Mini FAQ\n\n- **Isn't it better to do real logging?** Decorator-based logging isn't much different\n  from regular logging; think of these decorators as simply as a syntactic convenience.\n  Also, they are only handy in certain situations, not something to overuse.\n  The biggest benefit is it's less typing than a full log statement and worrying about\n  formatting: it handles the name of the function, the args and return values, etc and\n  it also truncates values so large values aren't logged by accident.\n\n- **If you want to trace function calls, shouldn't you use a debugger?** Decorator\n  logging doesn't replace a debugger.\n  It's just one more tool where you want some visibility with little effort.\n\n- **Doesn't this create tons of spam in your logs?** Again, this has all the usual\n  considerations of regular logging.\n  It will spam logs if you use it on functions that are called a lot.\n  In production, it tends to be useful either for slower or less frequently called\n  functions (like making an API call that takes a few seconds and consumes resources\n  anyway) or with the `if_slower_than_sec` option so it only logs unexpectedly slow\n  calls or with the tally options.\n\n- **Is this just a poor version of tracing?** The goal is to be as simple as possible,\n  even useful on little command-line apps.\n  If you're wanting proper visibility into function calls on production cloud-deployed\n  apps, you probably want something more appropriate, like OpenTelemetry.\n\n- **What about when you only want to log sometimes, not on every call?** Probably just\n  use a regular log statement.\n  Or only log tallies or use the `if_slower_than_sec` option.\n\n## Options\n\nThe `log_calls()` decorator is simple with reasonable defaults but is also fully\ncustomizable with optional arguments to the decorator.\nYou can control whether to show arg values and return values:\n\n- `show_args` to log the function arguments (truncating at `truncate_length`)\n\n- `show_return_value` to log the return value (truncating at `truncate_length`)\n\nBy default both calls and returns are logged, but this is also customizable:\n\n- `show_calls_only=True` to log only calls\n\n- `show_returns_only=True` to log only returns\n\n- `show_timing_only=True` only logs the timing of the call very briefly\n\nIf `if_slower_than_sec` is set, only log calls that take longer than that number of\nseconds.\n\nBy default, it uses standard logging with the given `level`, but you can pass in a\ncustom `log_func` to override that.\n\nAlso by default, it shows values using `quote_if_needed()`, which is brief and very\nreadable. You can pass in a custom `repr_func` to change that.\n\n## Alternatives\n\nThis is intended to only be a tiny library.\nIt doesn't replace any other logging framework or telemetry system.\n\nThe main alternative that is similarly simple is\n[logdecorator](https://github.com/sighalt/logdecorator).\nIt has similar use cases but a more explicit usage style, where you give the messages to\nthe decorator itself.\nIt seems like a good option but personally, I find that if I'm writing the log message,\nI'd often rather just use a regular log statement.\n(Also it does not offer tallies or timings like `funlog` does.)\n\n[Eliot](https://github.com/itamarst/eliot) is an interesting and more ambitious logging\nframework that includes somewhat similar decorator-based logging.\n\nThe main benefit of `funlog` is it is very simple to drop into a project when you want\nit and it's quick to add or remove a log decoration.\n\n* * *\n\n*This project was built from\n[simple-modern-uv](https://github.com/jlevy/simple-modern-uv).*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjlevy%2Ffunlog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjlevy%2Ffunlog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjlevy%2Ffunlog/lists"}