Orcraft 1.1.3 public beta released 08-Nov-2026. The Free Plan and the Team trial are open; card payments and invoices open shortly.

Orcraft 1.1.3 public beta released 08-Nov-2026. The Free Plan and the Team trial are open; card payments and invoices open shortly.

Orcraft

The platform for test development and orchestration
that lets you and your AI Agents run the entire testing operation

Watch the demo

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.

Type to search the docs.

    ↑↓ navigate ↵ open Esc close