Package: nhs-context-logging
NHS Context Loggingthis context logging library is designed to make adding good qualitystructuredlogs 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.
NOTE: when using context logging, logs are emitted when exiting the context (when the function call ends or wrapped context otherwise exits)quick startcontributingcontributors seecontributinginstallingpipinstallnhs-context-loggingloggingout of the box this framework will create structured logs with some default behaviours that we think work wellfromnhs_context_loggingimportapp_logger,log_action@log_action(log_reference="MYLOGREF",log_args=["my_arg"])defdo_a_thing(my_arg:int,another_arg:str):passif__name__=="__main__":app_logger.setup("mytool")action logginglog_action decoratoryou can decorate a function with thelog_actiondecoratorfromnhs_context_loggingimportlog_action@log_action(log_reference="MYLOGREF")defdo_a_thing(my_arg:int,another_arg:str):passout of the box this will give you rich logs associated with this logged action,
for example:timestamp - unix timestampinternal_id - a unique id preserved through nested log contextsaction_duration - action duration in secondsaction_status -  "succeeded|failed|error"  (failed/error being expected or unexpected exceptions .. see below)log_info - structure with log level, code path, line no, function name, thread, process id etc.{"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}}when decorating a function it's also easy to capture function argsfromnhs_context_loggingimportlog_action@log_action(log_reference="MYLOGREF",log_args=["my_arg"])defdo_a_thing(my_arg:int,another_arg:str):passadding log_args will capture named arguments and add to the logged action context. e.g.{"timestamp":1687435847.340391,"internal_id":"...","my_arg":12354}within a logged action context .. you can also add additional fields to logfromnhs_context_loggingimportlog_action,add_fieldsfrommylibimportanother_thing@log_action(log_reference="MYLOGREF",log_args=["my_arg"])defdo_a_thing(my_arg:int,another_arg:str):result=another_thing(my_arg)add_fields(another_thing_result=result)returnresultthis allows you to incrementally build up the data in a log line{"timestamp":1687435847.340391,"internal_id":"...","my_arg":12354,"another_thing_result":"win!"}You can also log the action result directly withlog_result=True(for non-generator functions):@log_action(log_reference="MYLOGREF",log_args=["my_arg"],log_result=True)defdo_a_thing(my_arg:int,another_arg:str):result=another_thing(my_arg)returnresultwhich appendsaction_resultto your log:{"timestamp":1687435847.340391,"internal_id":"...","my_arg":12354,"action_result":"win!"}Exceptions and ErrorsUnexpected Exceptionsby default the log action context will also log exceptions info when raised in a wrapped contextfromnhs_context_loggingimportlog_action@log_action(log_reference="MYLOGREF",log_args=["my_arg"])defdo_a_thing(my_arg:int,another_arg:str):raiseValueError(f'eek!{my_arg}')unexpected exceptions will result inaction_status=failedand include exception detail and INCLUDE STACK TRACE{"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..."}}Expected Exceptionswith theexpected_errorsargument, which takes atupleof types, you can use exceptions for business process flow without filling your logs with stack trace informationfromnhs_context_loggingimportlog_action@log_action(log_reference="MYLOGREF",log_args=["my_arg"],expected_errors=(ValueError,))defdo_a_thing(my_arg:int,another_arg:str):raiseValueError(f'eek!{my_arg}')expected exceptions are considered as business errors and will result inaction_status=errorand include exception detail and  NO STACK TRACE{"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..."}}testingthe library comes with a some pytest log capture fixtures ..# conftest.py# noinspection PyUnresolvedReferencesfromnhs_context_logging.fixturesimport*# noqa: F403# mytest.pyfromnhs_context_loggingimportadd_fields,log_action@log_action(log_args=["my_arg"])defanother_thing(my_arg)->bool:returnmy_arg==1@log_action(log_reference="MYLOGREF",log_args=["my_arg"])defdo_a_thing(my_arg:int,_another_arg:str):result=another_thing(my_arg)add_fields(result=result)returnresultdeftest_capture_some_logs(log_capture):std_out,std_err=log_captureexpected=1232212do_a_thing(expected,123)log=std_out[0]assertlog["action"]=="another_thing"assertlog["action_status"]=="succeeded"assertlog["my_arg"]==expectedlog=std_out[1]assertlog["action"]=="do_a_thing"assertlog["my_arg"]==expectedassertlog["action_status"]=="succeeded"assertlog["result"]isFalsetemporary global fieldsyou can also add in global fields (global to the the current log context that is)fromnhs_context_loggingimporttemporary_global_fields,log_action@log_action()defanother_thing():pass@log_action(log_reference="MYLOGREF",log_args=["my_arg"])defdo_a_thing(my_arg:int,another_arg:str):result=another_thing(my_arg)returnresult@log_action()defmain():withtemporary_global_fields(add_this_to_all_child_logs="AAAA"):do_a_thing(1234)will add theadd_this_to_all_child_logsfield to all child log contexts created within the global fields context manager.logger setupsimple setupfromnhs_context_loggingimportapp_logger,log_action@log_action()defmain():# this does the work with loggingpassif__name__=="__main__":app_logger.setup("my_awesome_app")main()async supportsince withasynciodifferent tasks will be running concurrently within the same thread, to ensure that logging works as intended  passingis_async=Truetoapp_logger.setupwill register the_TaskIsolatedContextStorageso action contexts within different async tasks will not interfere with each otherimportasynciofromnhs_context_loggingimportapp_logger,log_action@log_action()asyncdefchild_action():awaitasyncio.sleep(10)@log_action()asyncdefmain():# this does the work with loggingawaitchild_action()if__name__=="__main__":app_logger.setup("my_awesome_app",is_async=True)loop=asyncio.get_event_loop()loop.run_until_complete(main())traditional loggingthe logger also supports all the 'standard' logging interfaces ..fromnhs_context_loggingimportapp_loggerfrommylibimportanother_thingdefdo_a_thing(my_arg:int,another_arg:str):app_logger.info(message=f"started doing a thing, with args{my_arg}and{another_arg}")try:result=another_thing(my_arg)exceptExceptionaserr:app_logger.exception()raiseapp_logger.info(message=f"another thing result ={result}")returnresultbut will also accept an args dict as the first argfromnhs_context_loggingimportapp_loggerapp_logger.info(dict(arg1='aaa'))and a callable args sourcefromnhs_context_loggingimportapp_loggerapp_logger.info(lambda:dict(arg1='aaa'))or a mix with kwargsfromnhs_context_loggingimportapp_loggerdeflazy_args():returndict(message=1234,thing="bob")app_logger.info(lazy_args,log_reference="MESH1234")