Orcraft Logger
Most of an Orcraft log writes itself. Every component logs its config table when it starts and its
result table when it finishes, nested by level, and every
checkpoint logs its
outcome either way. OrcraftLogger is for everything a test wants to add on top of that.
from orcraft import OrcraftLogger as Logger
Logger.log("Starting the run", color=Logger.Color.BLUE)
Importing it as Logger is the convention throughout Orcraft suites, and it is what keeps
call sites short. Everything on it is a class method - there is no logger instance to create, pass
around or configure.
Messages
| Argument | Default | Meaning |
|---|---|---|
*args |
- | Values to log, joined with spaces, like print. |
color |
DEFAULT |
Foreground color from Logger.Color. |
bg_color |
DEFAULT |
Background color from Logger.Background. |
timestamp |
False |
Start the line with the time it was logged, as [2026-11-01 19:23:51.730], before the tag and the message. |
module_tag |
True |
Prefix the line with the calling module. Pass False for output that should read as the program's own, such as captured stdout. |
tab |
None |
Indent level, so a message nests with the component that emitted it. |
Logger.log(f"Setup: preparing {input_count} input files", color=Logger.Color.CYAN)
Logger.log(completed.stdout, module_tag=False)
Logger.log(completed.stderr, color=Logger.Color.RED, module_tag=False)
module_tag=False is the one worth knowing about. Output captured from the system under
test should appear as that program printed it; tagging it with the module that happened to relay it
makes the log harder to read, not easier.
Logger.log_error takes the same arguments, forces red and writes to stderr. It is for
reporting a problem, not for causing one - the result status is set by
checkpoints and by the
result descriptions, never by a log call.
Colors
Logger.Color covers the usual named colors and their light and dark variants, plus an
eleven-step gray ramp and a nine-step brown ramp. Every member is listed with a swatch in
Appendix: color reference.
To color part of a line rather than all of it, build the string with colorize:
parts.append(Logger.colorize(segment, Logger.Color.GREEN))
status_text = Logger.colorize_if(
label,
passed,
Logger.Color.GREEN,
Logger.Color.RED,
)
colorize returns the text wrapped in ANSI codes, so it composes into a larger message,
into an f-string, or into a table cell. colorize_if is the two-way version that most
status rendering ends up wanting.
The same mechanism drives result field formatters: a formatter that returns a colorized string is how a value arrives in the result table already colored.
Tables
The framework logs config and result tables on its own. When a test has tabular data of its own to report - a comparison, a per-item breakdown - it can log a table directly:
Logger.log_table(
headers=("Case", "Requested", "Measured", "Delta"),
rows=rows,
title="My Test Summary",
)
Columns are sized uniformly, the title is centered over them, and column_align and
column_widths are there when the defaults do not suit. tab works as it does
for log, so a table nests with the component that emitted it.
Whether the framework's own tables read horizontally or vertically is set per component with
log_orientation. Horizontal is compact for a handful of fields; vertical stays readable
when there are many, or when values are long; OFF drops the table from the log entirely.
The config class and the result class each carry their own log_orientation, so the two
tables are switched independently. A test's setting also reaches the tables the framework builds
beneath it: its cycles and cases take the test config's orientation for their Configuration tables,
and a case's Result table takes the orientation of the result class assigned to case.result,
or the test's own when none is assigned:
log_orientation = OrcraftConfigField.Format(
default=OrcraftConfigField.LogTableOrientation.VERTICAL
)
Progress and inline output
For work that takes long enough to need a sign of life, log_inline rewrites a single
console line instead of scrolling:
for index, item in enumerate(items, start=1):
Logger.log_inline_progress("Processing", index, len(items))
process(item)
Logger.log_inline_progress("Processing", len(items), len(items), finalize=True)
Call with finalize=True once at the end to terminate the line before other logs resume -
without it the next message lands on top of the progress bar. build_progress_bar returns
the bar and its fraction if you would rather compose the line yourself.
Inline output is a console convenience; the log file keeps the full record.
Log files
Console output is mirrored to a log file, with the ANSI decoration preserved - which is why a log
opened later, or rendered by the controller, looks the way the run looked. The framework initializes
the logger on first use, so a test never has to; Logger.get_log_file_path() returns the
file currently being written when a test needs to reference or attach it.
Appendix: color reference
Every member of Logger.Color is listed below. Logger.Background declares
exactly the same set of names, so anything valid as a foreground color is valid as a background
one:
Logger.log(
"Run finished",
color=Logger.Color.WHITE,
bg_color=Logger.Background.DARK_GREEN,
)
The swatches show how each color renders against a standard terminal palette. The 16-color members are resolved by whatever palette the terminal is configured with, so their exact appearance varies; the gray, brown and navy members are 256-color indices and are stable across terminals.
Named colors
| Member | Swatch | ANSI code |
|---|---|---|
DEFAULT |
- | - |
BLACK |
30 |
|
WHITE |
97 |
|
PURPLE |
35 |
|
GREEN |
92 |
|
DARK_GREEN |
32 |
|
LIGHT_GREEN |
92 |
|
YELLOW |
93 |
|
ORANGE |
33 |
|
DARK_ORANGE |
31 |
|
RED |
91 |
|
DARK_BLUE |
34 |
|
BLUE |
94 |
|
CYAN |
96 |
|
DARK_MAGENTA |
35 |
|
MAGENTA |
95 |
|
LIGHT_BLUE |
36 |
|
LIGHT_ORANGE |
33 |
|
NAVY |
38;5;18 |
|
DARK_NAVY |
38;5;17 |
A few names are aliases that resolve to the same code: GREEN and
LIGHT_GREEN, PURPLE and DARK_MAGENTA, ORANGE and
LIGHT_ORANGE. Note also that the orange family sits on the terminal's yellow and red
slots rather than on a true orange, which is the main reason those three render differently from one
terminal to the next. Where an exact shade matters, reach for the brown ramp instead.
DEFAULT emits no code at all, leaving the text in the terminal's own foreground color.
That is the value both color and bg_color take when you pass neither.
Gray ramp
Eleven steps from near-black to near-white, for output that should recede rather than compete - timestamps, context lines, values that are present but not the point.
| Member | Swatch | ANSI code |
|---|---|---|
GRAY0 |
38;5;232 |
|
GRAY10 |
38;5;234 |
|
GRAY20 |
38;5;236 |
|
GRAY30 |
38;5;238 |
|
GRAY40 |
38;5;240 |
|
GRAY50 |
38;5;242 |
|
GRAY60 |
38;5;244 |
|
GRAY70 |
38;5;246 |
|
GRAY80 |
38;5;248 |
|
GRAY90 |
38;5;250 |
|
GRAY100 |
38;5;255 |
Brown ramp
Nine steps through the reds, ambers and tans, useful when a log needs several distinguishable warm colors that the 16-color set cannot supply.
| Member | Swatch | ANSI code |
|---|---|---|
BROWN10 |
38;5;52 |
|
BROWN20 |
38;5;88 |
|
BROWN30 |
38;5;94 |
|
BROWN40 |
38;5;100 |
|
BROWN50 |
38;5;130 |
|
BROWN60 |
38;5;136 |
|
BROWN70 |
38;5;137 |
|
BROWN80 |
38;5;172 |
|
BROWN90 |
38;5;180 |
Background codes are the same indices with a 48;5; prefix in place of
38;5;, and the same 16-color codes offset into the background range - the framework
handles that translation, so a test only ever names the member.