Skip to main content

NHS Context Logging

this 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. NOTE: when using context logging, logs are emitted when exiting the context (when the function call ends or wrapped context otherwise exits)

quick start

contributing

contributors see contributing

installing

pip install nhs-context-logging

logging

out of the box this framework will create structured logs with some default behaviours that we think work well

from nhs_context_logging import app_logger, log_action

@log_action(log_reference="MYLOGREF", log_args=["my_arg"])
def do_a_thing(my_arg: int,  another_arg: str):
    pass

if __name__ == "__main__":
    app_logger.setup("mytool")

action logging

log_action decorator

you can decorate a function with the log_action decorator

from nhs_context_logging import log_action

@log_action(log_reference="MYLOGREF")
def do_a_thing(my_arg: int,  another_arg: str):
    pass

out of the box this will give you rich logs associated with this logged action, for example:

  • timestamp - unix timestamp
  • internal_id - a unique id preserved through nested log contexts
  • action_duration - action duration in seconds
  • action_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 args

from nhs_context_logging import log_action

@log_action(log_reference="MYLOGREF", log_args=["my_arg"])
def do_a_thing(my_arg: int,  another_arg: str):
    pass

adding 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 log

from nhs_context_logging import log_action, add_fields
from mylib import another_thing


@log_action(log_reference="MYLOGREF", log_args=["my_arg"])
def do_a_thing(my_arg: int,  another_arg: str):
    result = another_thing(my_arg)
    add_fields(another_thing_result=result)
    return result

this 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 with log_result=True (for non-generator functions):

@log_action(log_reference="MYLOGREF", log_args=["my_arg"], log_result=True)
def do_a_thing(my_arg: int, another_arg: str):
    result = another_thing(my_arg)
    return result

which appends action_result to your log:

{"timestamp": 1687435847.340391, "internal_id": "...", "my_arg": 12354, "action_result": "win!"}

Exceptions and Errors

Unexpected Exceptions

by default the log action context will also log exceptions info when raised in a wrapped context

from nhs_context_logging import log_action

@log_action(log_reference="MYLOGREF", log_args=["my_arg"])
def do_a_thing(my_arg: int,  another_arg: str):
    raise ValueError(f'eek! {my_arg}')

unexpected exceptions will result in action_status=failed and 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..."}}

failed action error info

Expected Exceptions

with 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

from nhs_context_logging import log_action

@log_action(log_reference="MYLOGREF", log_args=["my_arg"], expected_errors=(ValueError,))
def do_a_thing(my_arg: int,  another_arg: str):
    raise ValueError(f'eek! {my_arg}')

expected exceptions are considered as business errors and will result in action_status=error and 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..."}}

expected error info

testing

the library comes with a some pytest log capture fixtures ..

# conftest.py
# noinspection PyUnresolvedReferences
from nhs_context_logging.fixtures import *   # noqa: F403

# mytest.py
from nhs_context_logging import add_fields, log_action


@log_action(log_args=["my_arg"])
def another_thing(my_arg) -> bool:
    return my_arg == 1


@log_action(log_reference="MYLOGREF", log_args=["my_arg"])
def do_a_thing(my_arg: int, _another_arg: str):
    result = another_thing(my_arg)
    add_fields(result=result)
    return result


def test_capture_some_logs(log_capture):
    std_out, std_err = log_capture
    expected = 1232212
    do_a_thing(expected, 123)

    log = std_out[0]
    assert log["action"] == "another_thing"
    assert log["action_status"] == "succeeded"
    assert log["my_arg"] == expected

    log = std_out[1]
    assert log["action"] == "do_a_thing"
    assert log["my_arg"] == expected
    assert log["action_status"] == "succeeded"
    assert log["result"] is False

temporary global fields

you can also add in global fields (global to the the current log context that is)

from nhs_context_logging import temporary_global_fields, log_action


@log_action()
def another_thing():
    pass

@log_action(log_reference="MYLOGREF", log_args=["my_arg"])
def do_a_thing(my_arg: int,  another_arg: str):
    result = another_thing(my_arg)
    return result


@log_action()
def main():
    with temporary_global_fields(add_this_to_all_child_logs="AAAA"):
        do_a_thing(1234)

will add the add_this_to_all_child_logs field to all child log contexts created within the global fields context manager.

logger setup

simple setup

from nhs_context_logging import app_logger, log_action


@log_action()
def main():
    # this does the work with logging
    pass

if __name__ == "__main__":
    app_logger.setup("my_awesome_app")
    main()

async support

since 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

import asyncio
from nhs_context_logging import app_logger, log_action


@log_action()
async def child_action():
    await asyncio.sleep(10)

@log_action()
async def main():
    # this does the work with logging
    await child_action()

if __name__ == "__main__":
    app_logger.setup("my_awesome_app", is_async=True)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

traditional logging

the logger also supports all the 'standard' logging interfaces ..

from nhs_context_logging import app_logger
from mylib import another_thing

def do_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)
  except Exception as err:
      app_logger.exception()
      raise 
  app_logger.info(message=f"another thing result = {result}")
  return result
  

but will also accept an args dict as the first arg

from nhs_context_logging import app_logger
app_logger.info(dict(arg1='aaa'))

and a callable args source

from nhs_context_logging import app_logger
app_logger.info(lambda: dict(arg1='aaa'))

or a mix with kwargs

from nhs_context_logging import app_logger
def lazy_args():
    return dict(message=1234, thing="bob")

app_logger.info(lazy_args, log_reference="MESH1234")

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nhs_context_logging-0.7.40.tar.gz (28.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nhs_context_logging-0.7.40-py3-none-any.whl (19.1 kB view details)

Uploaded Python 3

File details

Details for the file nhs_context_logging-0.7.40.tar.gz.

File metadata

  • Download URL: nhs_context_logging-0.7.40.tar.gz
  • Upload date:
  • Size: 28.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.5.1 CPython/3.12.3 Linux/6.17.0-1020-azure

File hashes

Hashes for nhs_context_logging-0.7.40.tar.gz
Algorithm Hash digest
SHA256 7d67baa7ee50d06bdf8b3e3d14fbbec750490250acc9a0e7d0a4fd3a7839b14b
MD5 982b8e88d72bf1c01061d70fcc38bd9a
BLAKE2b-256 1c899dab30c4f39bee3515ab1abd3f635b6af76de2324cadc1d8ea5c9b4e025b

See more details on using hashes here.

File details

Details for the file nhs_context_logging-0.7.40-py3-none-any.whl.

File metadata

  • Download URL: nhs_context_logging-0.7.40-py3-none-any.whl
  • Upload date:
  • Size: 19.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.5.1 CPython/3.12.3 Linux/6.17.0-1020-azure

File hashes

Hashes for nhs_context_logging-0.7.40-py3-none-any.whl
Algorithm Hash digest
SHA256 d414472b9764dde5c8bc29f874ae72a4d990170c23ede23a375d8d648bde03dc
MD5 bc4e814d824a32cec8a1d6a99770d383
BLAKE2b-256 c80cf4d2167fe23873c8118e57b077a4c0f279aac750678c7ad4db3dd146f789

See more details on using hashes here.

Release history Release notifications | RSS feed

0.7.52

2 files

0.7.51

2 files

0.7.50

2 files

0.7.49

2 files

0.7.48

2 files

0.7.47

2 files

0.7.46

2 files

0.7.45

2 files

0.7.44

2 files

0.7.43

2 files

0.7.42

2 files

0.7.41

2 files

This release

0.7.40 This release

2 files

0.7.39

2 files

0.7.38

2 files

0.7.37

2 files

0.7.36

2 files

0.7.35

2 files

0.7.34

2 files

0.7.33

2 files

0.7.32

2 files

0.7.31

2 files

0.7.30

2 files

0.7.29

2 files

0.7.28

2 files

0.7.27

2 files

0.7.26

2 files

0.7.25

2 files

0.7.24

2 files

0.7.23

2 files

0.7.22

2 files

0.7.21

2 files

0.7.20

2 files

0.7.19

2 files

0.7.18

2 files

0.7.17

2 files

0.7.16

2 files

0.7.15

2 files

0.7.14

2 files

0.7.13

2 files

0.7.12

2 files

0.7.11

2 files

0.7.10

2 files

0.7.9

2 files

0.7.8

2 files

0.7.7

2 files

0.7.6

2 files

0.7.5

2 files

0.7.4

2 files

0.7.3

2 files

0.7.2

2 files

0.7.1

2 files

0.6.17

2 files

0.6.16

2 files

0.6.15

2 files

0.6.14

2 files

0.6.13

2 files

0.6.12

2 files

0.6.11

2 files

0.6.10

2 files

0.6.9

2 files

0.6.8

2 files

0.6.7

2 files

0.6.6

2 files

0.6.5

2 files

0.6.4

2 files

0.6.3

2 files

0.6.2

2 files

0.6.1

2 files

0.5.2

2 files

0.5.1

2 files

0.4.7

2 files

0.4.6

2 files

0.4.5

2 files

0.4.4

2 files

0.4.3

2 files

0.4.2

2 files

0.4.1

2 files

0.3.2

2 files

0.3.1

2 files

0.2.12

2 files

0.2.11

2 files

0.2.10

2 files

0.2.9

2 files

0.2.8

2 files

0.2.7

2 files

0.2.6

2 files

0.2.5

2 files

0.2.4

2 files

0.2.3

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page