{"id":23421161,"url":"https://github.com/nhsdigital/nhs-context-logging","last_synced_at":"2026-03-05T17:38:13.480Z","repository":{"id":176403073,"uuid":"656691615","full_name":"NHSDigital/nhs-context-logging","owner":"NHSDigital","description":null,"archived":false,"fork":false,"pushed_at":"2026-02-05T16:46:49.000Z","size":534,"stargazers_count":0,"open_issues_count":2,"forks_count":2,"subscribers_count":1,"default_branch":"develop","last_synced_at":"2026-02-05T21:20:04.426Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/NHSDigital.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-06-21T12:48:26.000Z","updated_at":"2026-02-05T16:53:53.000Z","dependencies_parsed_at":"2023-11-21T20:29:31.995Z","dependency_job_id":"de6d1647-1b70-4020-9493-97c0bae0d218","html_url":"https://github.com/NHSDigital/nhs-context-logging","commit_stats":null,"previous_names":["nhsdigital/nhs-context-logging"],"tags_count":66,"template":false,"template_full_name":null,"purl":"pkg:github/NHSDigital/nhs-context-logging","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NHSDigital%2Fnhs-context-logging","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NHSDigital%2Fnhs-context-logging/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NHSDigital%2Fnhs-context-logging/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NHSDigital%2Fnhs-context-logging/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/NHSDigital","download_url":"https://codeload.github.com/NHSDigital/nhs-context-logging/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NHSDigital%2Fnhs-context-logging/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30139370,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-05T16:58:46.102Z","status":"ssl_error","status_checked_at":"2026-03-05T16:58:45.706Z","response_time":93,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":"2024-12-23T02:14:14.726Z","updated_at":"2026-03-05T17:38:13.390Z","avatar_url":"https://github.com/NHSDigital.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NHS Context Logging\n\nthis context logging library is designed to make adding good quality *structured* logs EASY and make source code easy to read, removing 'boilerplate' logging from the code and allowing the eye to focus on what the code does.\nNOTE: when using context logging, logs are emitted when exiting the context (when the function call ends or wrapped context otherwise exits)\n\n# quick start\n\n### contributing\ncontributors see [contributing](CONTRIBUTING.md)\n\n### installing\n\n```shell\npip install nhs-context-logging\n```\n\n### logging\nout of the box this framework will create structured logs with some default behaviours that we think work well\n\n```python\nfrom nhs_context_logging import app_logger, log_action\n\n@log_action(log_reference=\"MYLOGREF\", log_args=[\"my_arg\"])\ndef do_a_thing(my_arg: int,  another_arg: str):\n    pass\n\nif __name__ == \"__main__\":\n    app_logger.setup(\"mytool\")\n```\n\n\n## action logging\n\n\n\n###  log_action decorator\n\nyou can decorate a function with the `log_action` decorator\n```python\nfrom nhs_context_logging import log_action\n\n@log_action(log_reference=\"MYLOGREF\")\ndef do_a_thing(my_arg: int,  another_arg: str):\n    pass\n\n```\n\nout of the box this will give you rich logs associated with this logged action,\nfor example:\n- timestamp - unix timestamp\n- internal_id - a unique id preserved through nested log contexts\n- action_duration - action duration in seconds\n- action_status -  \"succeeded|failed|error\"  (failed/error being expected or unexpected exceptions .. see below)\n- log_info - structure with log level, code path, line no, function name, thread, process id etc. \n\n```json\n{\"timestamp\": 1687435847.340391, \"internal_id\": \"d2c867f7f89a4a10b3257355dc558447\", \"action_duration\": 0.00001, \"action_status\": \"succeeded\", \"log_info\": {\"level\": \"INFO\", \"path\": \"/home/zaphod/spine/nhs-context-logging/tests/logger_tests.py\", \"line_no\": 171, \"func\": \"do_a_thing\", \"pid\": 4118793, \"thread\": 139808108545856}}\n```\n\nwhen decorating a function it's also easy to capture function args\n```python\nfrom nhs_context_logging import log_action\n\n@log_action(log_reference=\"MYLOGREF\", log_args=[\"my_arg\"])\ndef do_a_thing(my_arg: int,  another_arg: str):\n    pass\n\n```\n\nadding log_args will capture named arguments and add to the logged action context. e.g.\n```json\n{\"timestamp\": 1687435847.340391, \"internal_id\": \"...\", \"my_arg\": 12354}\n```\n\n\nwithin a logged action context .. you can also add additional fields to log \n```python\nfrom nhs_context_logging import log_action, add_fields\nfrom mylib import another_thing\n\n\n@log_action(log_reference=\"MYLOGREF\", log_args=[\"my_arg\"])\ndef do_a_thing(my_arg: int,  another_arg: str):\n    result = another_thing(my_arg)\n    add_fields(another_thing_result=result)\n    return result\n\n```\n\nthis allows you to incrementally build up the data in a log line \n```json\n{\"timestamp\": 1687435847.340391, \"internal_id\": \"...\", \"my_arg\": 12354, \"another_thing_result\": \"win!\"}\n```\n\nYou can also log the action result directly with `log_result=True` (for non-generator functions):\n\n```python \n@log_action(log_reference=\"MYLOGREF\", log_args=[\"my_arg\"], log_result=True)\ndef do_a_thing(my_arg: int, another_arg: str):\n    result = another_thing(my_arg)\n    return result\n```\n\nwhich appends `action_result` to your log:\n```json\n{\"timestamp\": 1687435847.340391, \"internal_id\": \"...\", \"my_arg\": 12354, \"action_result\": \"win!\"}\n```\n\n### Exceptions and Errors\n\n#### Unexpected Exceptions\nby default the log action context will also log exceptions info when raised in a wrapped context\n\n```python\nfrom nhs_context_logging import log_action\n\n@log_action(log_reference=\"MYLOGREF\", log_args=[\"my_arg\"])\ndef do_a_thing(my_arg: int,  another_arg: str):\n    raise ValueError(f'eek! {my_arg}')\n```\nunexpected exceptions will result in `action_status=failed` and include exception detail and INCLUDE STACK TRACE\n```json\n{\"timestamp\": 1687435847.340391, \"action_status\": \"failed\", \"internal_id\": \"...\", \"my_arg\": 12354,\"error_info\": { \"args\": [1,2,3], \"error\": \"ValueError('eek 1232')\", \"line_no\": 69, \"traceback\": \"file '/home...\"}}\n```\n\n![failed action error info](img-action-failed.png)\n\n#### Expected Exceptions\nwith the `expected_errors` argument, which takes a `tuple` of types, you can use exceptions for business process flow without filling your logs with stack trace information\n\n```python\nfrom nhs_context_logging import log_action\n\n@log_action(log_reference=\"MYLOGREF\", log_args=[\"my_arg\"], expected_errors=(ValueError,))\ndef do_a_thing(my_arg: int,  another_arg: str):\n    raise ValueError(f'eek! {my_arg}')\n```\nexpected exceptions are considered as business errors and will result in `action_status=error` and include exception detail and  NO STACK TRACE\n```json\n{\"timestamp\": 1687435847.340391, \"action_status\": \"failed\", \"internal_id\": \"...\", \"my_arg\": 12354,\"error_info\": { \"args\": [1,2,3], \"error\": \"ValueError('eek 1232')\", \"line_no\": 69, \"traceback\": \"file '/home...\"}}\n```\n\n\n![expected error info](img-action-error.png)\n\n## testing\n\nthe library comes with a some pytest log capture fixtures .. \n\n\n```python\n# conftest.py\n# noinspection PyUnresolvedReferences\nfrom nhs_context_logging.fixtures import *   # noqa: F403\n\n# mytest.py\nfrom nhs_context_logging import add_fields, log_action\n\n\n@log_action(log_args=[\"my_arg\"])\ndef another_thing(my_arg) -\u003e bool:\n    return my_arg == 1\n\n\n@log_action(log_reference=\"MYLOGREF\", log_args=[\"my_arg\"])\ndef do_a_thing(my_arg: int, _another_arg: str):\n    result = another_thing(my_arg)\n    add_fields(result=result)\n    return result\n\n\ndef test_capture_some_logs(log_capture):\n    std_out, std_err = log_capture\n    expected = 1232212\n    do_a_thing(expected, 123)\n\n    log = std_out[0]\n    assert log[\"action\"] == \"another_thing\"\n    assert log[\"action_status\"] == \"succeeded\"\n    assert log[\"my_arg\"] == expected\n\n    log = std_out[1]\n    assert log[\"action\"] == \"do_a_thing\"\n    assert log[\"my_arg\"] == expected\n    assert log[\"action_status\"] == \"succeeded\"\n    assert log[\"result\"] is False\n\n\n\n```\n\n\n\n## temporary global fields\n\nyou can also add in global fields (global to the the current log context that is)\n\n```python\nfrom nhs_context_logging import temporary_global_fields, log_action\n\n\n@log_action()\ndef another_thing():\n    pass\n\n@log_action(log_reference=\"MYLOGREF\", log_args=[\"my_arg\"])\ndef do_a_thing(my_arg: int,  another_arg: str):\n    result = another_thing(my_arg)\n    return result\n\n\n@log_action()\ndef main():\n    with temporary_global_fields(add_this_to_all_child_logs=\"AAAA\"):\n        do_a_thing(1234)\n\n```\nwill add the `add_this_to_all_child_logs` field to all child log contexts created within the global fields context manager.\n\n\n## logger setup\n\n### simple setup\n\n```python\nfrom nhs_context_logging import app_logger, log_action\n\n\n@log_action()\ndef main():\n    # this does the work with logging\n    pass\n\nif __name__ == \"__main__\":\n    app_logger.setup(\"my_awesome_app\")\n    main()\n```\n\n### async support\n\nsince with `asyncio` different tasks will be running concurrently within the same thread, to ensure that logging works as intended  passing `is_async=True` to `app_logger.setup` will register the `_TaskIsolatedContextStorage` so action contexts within different async tasks will not interfere with each other\n\n```python\nimport asyncio\nfrom nhs_context_logging import app_logger, log_action\n\n\n@log_action()\nasync def child_action():\n    await asyncio.sleep(10)\n\n@log_action()\nasync def main():\n    # this does the work with logging\n    await child_action()\n\nif __name__ == \"__main__\":\n    app_logger.setup(\"my_awesome_app\", is_async=True)\n    loop = asyncio.get_event_loop()\n    loop.run_until_complete(main())\n```\n\n\n\n## traditional logging\nthe logger also supports all the 'standard' logging interfaces ..\n```python\nfrom nhs_context_logging import app_logger\nfrom mylib import another_thing\n\ndef do_a_thing(my_arg: int, another_arg: str):\n  app_logger.info(message=f\"started doing a thing, with args {my_arg} and {another_arg}\")\n  try:\n    result = another_thing(my_arg)\n  except Exception as err:\n      app_logger.exception()\n      raise \n  app_logger.info(message=f\"another thing result = {result}\")\n  return result\n  \n```\n\nbut will also accept an args dict as the first arg\n```python\n\nfrom nhs_context_logging import app_logger\napp_logger.info(dict(arg1='aaa'))\n```\n\nand a callable args source \n\n```python\n\nfrom nhs_context_logging import app_logger\napp_logger.info(lambda: dict(arg1='aaa'))\n```\n\nor a mix with kwargs\n```python\nfrom nhs_context_logging import app_logger\ndef lazy_args():\n    return dict(message=1234, thing=\"bob\")\n\napp_logger.info(lazy_args, log_reference=\"MESH1234\")\n\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnhsdigital%2Fnhs-context-logging","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnhsdigital%2Fnhs-context-logging","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnhsdigital%2Fnhs-context-logging/lists"}