[![GitHub release; latest by date](https://img.shields.io/github/v/release/SETI/rms-pdslogger)](https://github.com/SETI/rms-pdslogger/releases)
[![GitHub Release Date](https://img.shields.io/github/release-date/SETI/rms-pdslogger)](https://github.com/SETI/rms-pdslogger/releases)
[![Test Status](https://img.shields.io/github/actions/workflow/status/SETI/rms-pdslogger/run-tests.yml?branch=main)](https://github.com/SETI/rms-pdslogger/actions)
[![Documentation Status](https://readthedocs.org/projects/rms-pdslogger/badge/?version=latest)](https://rms-pdslogger.readthedocs.io/en/latest/?badge=latest)
[![Code coverage](https://img.shields.io/codecov/c/github/SETI/rms-pdslogger/main?logo=codecov)](https://codecov.io/gh/SETI/rms-pdslogger)
<br />
[![PyPI - Version](https://img.shields.io/pypi/v/rms-pdslogger)](https://pypi.org/project/rms-pdslogger)
[![PyPI - Format](https://img.shields.io/pypi/format/rms-pdslogger)](https://pypi.org/project/rms-pdslogger)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/rms-pdslogger)](https://pypi.org/project/rms-pdslogger)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/rms-pdslogger)](https://pypi.org/project/rms-pdslogger)
<br />
[![GitHub commits since latest release](https://img.shields.io/github/commits-since/SETI/rms-pdslogger/latest)](https://github.com/SETI/rms-pdslogger/commits/main/)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/SETI/rms-pdslogger)](https://github.com/SETI/rms-pdslogger/commits/main/)
[![GitHub last commit](https://img.shields.io/github/last-commit/SETI/rms-pdslogger)](https://github.com/SETI/rms-pdslogger/commits/main/)
<br />
[![Number of GitHub open issues](https://img.shields.io/github/issues-raw/SETI/rms-pdslogger)](https://github.com/SETI/rms-pdslogger/issues)
[![Number of GitHub closed issues](https://img.shields.io/github/issues-closed-raw/SETI/rms-pdslogger)](https://github.com/SETI/rms-pdslogger/issues)
[![Number of GitHub open pull requests](https://img.shields.io/github/issues-pr-raw/SETI/rms-pdslogger)](https://github.com/SETI/rms-pdslogger/pulls)
[![Number of GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/SETI/rms-pdslogger)](https://github.com/SETI/rms-pdslogger/pulls)
<br />
![GitHub License](https://img.shields.io/github/license/SETI/rms-pdslogger)
[![Number of GitHub stars](https://img.shields.io/github/stars/SETI/rms-pdslogger)](https://github.com/SETI/rms-pdslogger/stargazers)
![GitHub forks](https://img.shields.io/github/forks/SETI/rms-pdslogger)

# Introduction

`pdslogger` provides a new class and associated functions that augment the functionality
of the standard Python `logging` module.

`pdslogger` is a product of the [PDS Ring-Moon Systems Node](https://pds-rings.seti.org).

# Installation

The `pdslogger` module is available via the
`rms-pdslogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://pypi.org/project/rms-pdslogger) package on PyPI and can be
installed with:

```sh
pip install rms-pdslogger
```

# Getting Started

The
`PdsLogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger)
class provides a variety of enhancements to Python's
`logging.Logger` class. Use the
`PdsLogger()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger)
constructor or method
`get_logger()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.get_logger)
to create a new logger or access an existing one by name. To
add the capabilities of this class to an existing ``logging.Logger``, use
`PdsLogger.as_pdslogger()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.as_pdslogger).
In this case, the behavior of the new
`PdsLogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger)
will be identical to that of the given ``Logger``, including all formatting, but the
additional capabilities of this class will also become available.

`PdsLogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger)
supports hierarchical logs, in which the log generated by a running task can be cleanly
separated into individual sub-tasks. When a sub-task is completed, the log contains a
summary of the number of logged messages by category and, optionally, the elapsed time.
You create a new level in the hierarchy with
`open()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.open)
and close it with
`close()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.close).
Alternatively, you can write:

```python
with logger.open(...):
    # Write log messages here
```

to close the newly-opened section of the log automatically. A specific handler can be
assigned to the
`PdsLogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger)
as part of the
`open()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.open)
call and it
will be removed upon closing. Use this feature, for example, to obtain separate log files
for individual tasks or when processing a sequence of individual files.

The constructor allows the user to create additional error categories beyond the standard
ones named "debug", "info", "warn", etc. Each category can be assigned its own
numeric level, where DEBUG=10, INFO=20, WARNING=30, ERROR=40, and FATAL=50. A level of
None or HIDDEN means that messages with this alias are always suppressed.

The following additional message categories are used widely by the RMS Node and are
defined by default:

* "normal" (default level 20=INFO) is used for any normal outcome.
* "header" (default level 20=INFO) is used for message headers following
  `open()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.open)
and summary messages following
  `close()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.close).
* "exception" (default level 50=FATAL) is used when an exception is encountered.
* "ds_store" (default level 10=DEBUG) is used when a task encounters a ".DS_Store" file
  as managed by the MacOS Finder.
* "dot_" (default level 40=ERROR) is used when a file beginning with "._" is
  encountered. These files are sometimes created by "tar" commands in MacOS.
* "invisible" (default level 30=WARN) is used if any other invisible file is
  encountered.

Of course, any of these default levels can be modified on a logger-by-logger basis.

Use
`set_limit()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.set_limit)
to specify a limit on the number of messages that can be
associated with an alias. For example, if the limit on "info" messages is 100, then log
messages after the hundredth will be suppressed, although the number of suppressed
messages will still be tracked. At the end of the log, a tally of the messages associated
with each alias is printed, including the number suppressed if the limit was exceeded.

`PdsLogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger)
supports a rich set of formatting options for the log records, which
can be specified in the constructor or by using
`set_format()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.set_format).
By
default, each log record automatically includes a time tag, log name, level, text message,
and optional file path. You can also use
`add_root()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.add_root)
and related methods to
exercise more control over how file paths appear.

Use
`log()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.log)
to log a message, or level-specific methods such as
`debug()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.debug),
`info()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.info),
`warning()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.warning),
`error()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.error),
`critical()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.critical),
etc. Each
of these methods receives a message string and arguments identical to the methods of the
same name in `logging.Logger`. However, they also take an optional file path, which is
automatically formatted to appear after the message text in the log.

Method
`exception()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.exception)
can be used inside a Python **except** clause to write an
exception message into the log, including the stacktrace. If you desire greater control
over how an exception is recorded in the log, you can use the
`LoggerException`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.LoggerException)
class or define your own subclass.

Simple tools are also provided to create handlers to assign to a
`PdsLogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger)
using
`add_handler()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger.add_handler)
and related methods:

* `file_handler()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.file_handler)
  is a function that provides a rich set of options for constructing
  `logging.FileHandler` objects, which allow logs to be written to a file. The options
  include version numbering, appending a date or time to the file name, and daily
  rotations.
  `file_handler()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.file_handler)
  also supports the RMS Node's
  `rms-filecache`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://pypi.org/project/rms-filecache)
  module, which
  allows log files to be seamlessly saved into cloud storage; simply pass in a URI or
  `FCPath`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-filecache.readthedocs.io/en/latest/module.html#filecache.file_cache_path.FCPath)
  object instead of a local file path.
* `info_handler()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.info_handler),
  `warning_handler()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.warning_handler),
  and
  `error_handler()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.error_handler)
  are
  simpler versions of the above, in which the level of message logging is implied.
* `stream_handler()`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.stream_handler)
  is a function that creates handlers to write to an I/O
  stream such as **sys.stdout** or **sys.stderr**.
* ``STDOUT_HANDLER`` is a pre-defined handler that prints all output to the terminal.
* ``NULL_HANDLER`` is a pre-defined handler that suppresses all output.

Note that a `PdsLogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger)
will print message to the terminal if no handler has been assigned
to it. As a result, if you really wish to not see any messages, you must assign it the
`NULL_HANDLER`.

In the Macintosh Finder, log files are color-coded by the most severe message encountered
within the file: green for info, yellow for warnings, red for errors, and violet for
fatal or critical errors.

For extremely simple logging needs, four subclasses of
`PdsLogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.PdsLogger)
are provided.

* `EasyLogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.EasyLogger)
  prints all messages above a specified level of severity to the
  terminal.
* `ErrorLogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.ErrorLogger)
  only prints error messages.
* `CriticalLogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.CriticalLogger)
  only prints exceptions and other "fatal" messages.
* `NullLogger`[![image](https://raw.githubusercontent.com/SETI/rms-pdslogger/main/icons/link.png)](https://rms-pdslogger.readthedocs.io/en/latest/module.html#pdslogger.NullLogger)
  suppresses all messages, including logged exceptions.

These four subclasses have the common trait that they cannot be assigned handlers.

Details of each function and class are available in the [module
documentation](https://rms-pdslogger.readthedocs.io/en/latest/module.html).

This simple example:

```
import pdslogger
logger = pdslogger.PdsLogger('sample', parent='test')
logger.warning('Warning message')
with logger.open('Sub-log'):
    logger.debug('Debug message level 2')
logger.close()
```

will yield:

```
2024-12-04 13:47:37.004203 | test.sample || WARNING | Warning message
2024-12-04 13:47:37.004224 | test.sample || HEADER | Sub-log
2024-12-04 13:47:37.004240 | test.sample |-| DEBUG | Debug message level 2
2024-12-04 13:47:37.004270 | test.sample || SUMMARY | Completed: Sub-log
2024-12-04 13:47:37.004276 | test.sample || SUMMARY | Elapsed time = 0:00:00.000016
2024-12-04 13:47:37.004280 | test.sample || SUMMARY | 1 DEBUG message

2024-12-04 13:47:37.004295 | test.sample || SUMMARY | Completed: test.sample
2024-12-04 13:47:37.004299 | test.sample || SUMMARY | Elapsed time = 0:00:00.000094
2024-12-04 13:47:37.004302 | test.sample || SUMMARY | 1 WARNING message
2024-12-04 13:47:37.004305 | test.sample || SUMMARY | 1 DEBUG message

```

# Contributing

Information on contributing to this package can be found in the
[Contributing Guide](https://github.com/SETI/rms-pdslogger/blob/main/CONTRIBUTING.md).

# Links

- [Documentation](https://rms-pdslogger.readthedocs.io)
- [Repository](https://github.com/SETI/rms-pdslogger)
- [Issue tracker](https://github.com/SETI/rms-pdslogger/issues)
- [PyPi](https://pypi.org/project/rms-pdslogger)

# Licensing

This code is licensed under the [Apache License v2.0](https://github.com/SETI/rms-pdslogger/blob/main/LICENSE).
