Logging¶
Observability is an important feature in HyperShell. Logging is a big part of understanding what is happening within your workflow. We want to know when events occur, like when something starts, why something failed, or if a condition is met.
The program emits messages with different levels of severity. Unless otherwise configured,
the user will see INFO, WARNING, ERROR, or CRITICAL messages. Setting the logging.level
determines which messages are emitted; only messages with a severity equal to or greater than
the current level will be shown. For example, by default the level is set to INFO and so
only messages with a severity equal to or higher than that will be shown.
See also the logging section in the configuration parameter reference.
Levels¶
All severity levels are shown and described below in order of highest to lowest level.
- CRITICAL
These message are only emitted when the program will halt. Typically this is at the very beginning because of an issue with command-line arguments. It could be due to a failure on the system or because the program was sent an interrupt signal.
- ERROR
These messages are emitted when a fault has occurred but the program does not need to halt. For example, when the program attempts to terminate a running task that has exceeded the configured walltime limit and that task fails to actually halt.
- WARNING
These messages are emitted in many circumstances where a notice to the user is warranted but not because of a failure in the program. For example, a non-zero exit status from one of the tasks is expected behavior but warrants notifying the user.
- INFO
These messages are only emitted by clients at the start of a task. The idea is that under normal, stable operations the user should only see one message per task.
- DEBUG
These messages are emitted for any number of events within the operations of the system. Anytime an action occurs, a DEBUG message is emitted. For example, server-side and client-side task bundle operations, thread start and stop, and program transitions.
- TRACE
These messages are emitted for higher frequency activity not included in DEBUG. These are typically cycling, waiting behavior. For example, individualized task movement in the system, as well as polling behavior.
Note
For developers, there is yet a deeper level, DEVEL, unused and otherwise undocumented in the
released software. Within the hypershell.core.fsm module, developers can
enable these messages to log state transitions in all program threads along with a fuzzer to
randomize delays in these transitions.
Formatting¶
The user has complete control of what is included in the messages and how they are structured and formatted. HyperShell is written in the Python programming language and uses the standard logging facility. Messages can include many other contextual attributes along side the message itself; we extend these to include a few others.
Defining these formats can be cumbersome and in the majority of cases users will not want to fiddle with these as they are not human friendly. As such we’ve pre-defined a number of styles to make it easier to switch between a number of standard formats.
Nevertheless, here is an example of setting a basic format.
Configuration file with logging format
[logging]
level = "info"
format = "[%(asctime)s %(levelname)s] %(message)s"
Instead of defining the format directly, we can refer to one of the following styles.
Style |
Format |
|---|---|
|
|
|
|
|
|
|
|
The default style is aptly named as it is the default format used by HyperShell. It includes
rich color and formatting (see note about NO_COLOR). Only the level, module name, and the message
itself are included. This is a good starting point for basic work as all the other details are more
suited for batch, pipeline work and only gets in the way initially.
The detailed style expands on default to include a precise timestamp including milliseconds,
and the hostname of the machine the message originated from.
The detailed-compact includes the same information as detailed, but in a compacted form.
The timestamp is relative elapsed time since program start, and both the module and hostname are
shorter/relative. So hypershell. is dropped from the module name and hostnames will only be the
specific node name if operating in a cluster environment within a given subnet (e.g., a123 instead
of a123.cluster.univ.edu).
The system format is similar to detailed but explicitly disables colorization and includes
the specific UUID of each instance of the program operating in the cluster. This format is useful when
operating as a system service.
Note
The ANSI escape sequences injected into the logging output work well and are compatible with all major platforms, not only UNIX-like systems but also in the modern Windows terminal.
These sequences are only emitted if and only if the connected stderr channel is a TTY. Essentially, if your process is connected to a live terminal session we allow formatting. Otherwise it is automatically disabled; e.g., in a UNIX-pipeline or redirect.
If you like the available style you are using and simply do not want the colors and formatting,
you can disable them manually by defining the NO_COLOR environment variable.
See no-color.org for an understanding of this convention.
To make this change permanent, put this in your shell login profile (e.g., ~/.bashrc).
Conversely, if the non-TTY aspect is disabling color but you want to keep them for whatever
reason you can force colors regardless of the connected output channel by defining the
FORCE_COLOR environment variable.
The following is a table of extra attributes defined by HyperShell beyond what is described in the Python logging documentation.
Format |
Description |
|---|---|
|
Application-level instance UUID. Clients tend to be identified by their hostname, but that may not be distinct at once or over time. |
|
Hostname (e.g., |
|
Shortened hostname (e.g., |
|
Module name without package (e.g., |
|
Relative time elapsed since start of program formatted as integer number of seconds. |
|
Relative time elapsed since start of program formatted as integer number of milliseconds. |
|
Relative time elapsed since start of program formatted
in automatically (e.g., |
|
Relative time elapsed since start of program formatted
in hour, minutes, and seconds: |
|
ANSI escape sequence associated with message level
(e.g., if the current message has level INFO then
this will correspond to |
|
ANSI escape sequence for reset. |
|
ANSI escape sequence for bold. |
|
ANSI escape sequence for faint. |
|
ANSI escape sequence for italic. |
|
ANSI escape sequence for underline. |
|
ANSI escape sequence for black. |
|
ANSI escape sequence for red. |
|
ANSI escape sequence for green. |
|
ANSI escape sequence for yellow. |
|
ANSI escape sequence for blue. |
|
ANSI escape sequence for magenta. |
|
ANSI escape sequence for cyan. |
|
ANSI escape sequence for white. |
File-based Logging¶
By default HyperShell writes log messages only to the console (stderr). For
long-running servers and clusters, and for unattended batch pipelines, it is often
preferable to also persist messages to disk with automatic rotation and compression
so the logs never grow without bound. File-based logging is opt-in and operates
independently of the console: it has its own severity level and format, so you can keep
a quiet, human-friendly console while retaining a verbose, machine-parsable record on disk.
File-based logging is enabled the moment any logging.file parameter is set. The
simplest form enables it with all defaults, writing to the default per-process file
described below (rotation and compression disabled):
Enable file-based logging
[logging]
file = "enabled" # or true, or an explicit path such as "/var/log/hypershell/hs.log"
For full control, define the [logging.file] section and set individual parameters:
Configuration file with rotating, compressed logs
[logging.file]
level = "debug" # captured to disk independently of the console level
rotate = "512MB" # rotate once the active file reaches 512 MiB
compress = "gzip" # compress each rotated file in the background
keep = 2 # keep 2 uncompressed rotations alongside the archives
Per-process log files¶
Every process writes to its own file, named for its role and host. This is essential in a distributed cluster: many clients — potentially on shared storage — would otherwise contend for a single rotating file, renaming, compressing, and pruning it out from under one another. The default file for each process is:
Process |
Default file |
|---|---|
|
|
|
|
|
|
|
|
other commands, library use |
|
Here <host> is the short hostname. Files are written into the log directory of
your site. If two processes of the same role run on one host at once, the
second claims a numbered slot (client-<host>-2.log, -3, and so on); when a process
exits — even by crashing — its slot is immediately reused by the next one, so the number of
files tracks peak concurrency on a host, not the total number of processes ever launched
(important under autoscaling). Because names never collide across roles or hosts, the entire
log directory can be shipped or merged with ordinary tools — e.g. rsync to collect
per-node logs, or sort -m to interleave them into a single timeline.
If you set an explicit path it is honored verbatim, with one exception: for the
client role — which is launched en masse across a cluster — the hostname is appended so
the mass-launched clients still never share a file.
Parameters¶
pathDestination file path (default: the per-process file described above). A relative path is resolved against the current working directory; parent directories are created as needed.
levelMinimum severity written to the file (default:
trace). Accepts the same names aslogging.level. This is independent of the console level, so the default captures everything to disk while the console stays atinfo.styleMessage format preset for the file (default:
system). Accepts the same presets aslogging.style(default,detailed,detailed-compact,system); aformatstring may be given instead. ANSI color is always stripped from file output. The defaultsystemstyle stamps every line with a timestamp, hostname, and instanceapp_id, which is what makes merged, multi-file logs sortable and traceable.rotateRotation policy (default:
never). See Rotation below.compressCompression applied to rotated files (default: none). See Compression and retention below.
keepNumber of uncompressed rotated files to retain on disk (default:
0). See Compression and retention below.
Rotation¶
The rotate policy accepts three kinds of value:
neverNo rotation (the default); the file grows indefinitely.
- size-like
A byte threshold such as
512MBor2GB: the active file is rotated once it reaches that size. Accepted units areKB,MB,GB, andTB— each a power of 1024 — and a bare number is interpreted as bytes. Rotated files are numbered (name.1,name.2, …).- time-like
A cron expression (requires the
croniterpackage) such as@hourly,@daily,@midnight, or0 1 * * 0: the file is rotated on that schedule.@dailyand@midnightname rotated files by date (name.YYYYMMDD); any other expression also includes the time (name.YYYYMMDD-HHMMSS).
Regardless of policy, sending SIGHUP to a process triggers an immediate rotation of its
log file. This is the conventional way to rotate on demand — for example from an external
scheduler or a logrotate-style tool. See the signals section.
Compression and retention¶
When compress is set, each rotated file is compressed in the background. Accepted formats
(and the extension applied) are gzip (.gz), bzip (.bz2), and lzma
(.xz) from the Python standard library, plus zstd (.zstd), which requires the
optional zstandard package.
The keep parameter sets how many uncompressed rotated files to leave on disk; older
uncompressed rotations beyond this count are removed once they have been compressed. The
compressed archives themselves are always retained.
Note
Rotated files are only ever deleted when compression is enabled. With compress unset,
rotated files accumulate and keep has no effect — enable compression, or prune the
directory yourself, to bound disk usage.
Uncaught Exceptions and Tracebacks¶
If for whatever reason the program crashes with an unexpected fault, we stash the full Python
traceback in a file within the default logging directory. See the section on file system
paths under configuration for details. This will be in the system location
if the program is run as root or the user location, unless the HYPERSHELL_SITE variable
is set, which will take precedence.
We always log a CRITICAL message with the path to the created file.