Add foundation logging facade

This commit is contained in:
2026-06-01 08:20:58 +02:00
parent 6604f30ef3
commit a7bb04f54b
9 changed files with 262 additions and 6 deletions

View File

@@ -69,6 +69,7 @@ add_custom_target(panopainter_validate_shaders
add_library(pp_foundation STATIC add_library(pp_foundation STATIC
src/foundation/binary_stream.cpp src/foundation/binary_stream.cpp
src/foundation/log.cpp
src/foundation/parse.cpp src/foundation/parse.cpp
src/foundation/trace.cpp) src/foundation/trace.cpp)
target_include_directories(pp_foundation target_include_directories(pp_foundation

View File

@@ -80,7 +80,8 @@ Known local toolchain state:
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`, `platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`, `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`,
`pp_ui_core`, `pano_cli`, and their current headless test binaries, `pp_ui_core`, `pano_cli`, and their current headless test binaries,
including PPI header, paint stroke sampling, and layout XML parse coverage. including foundation logging, PPI header, paint stroke sampling, and layout
XML parse coverage.
- `panopainter_validate_shaders` validates the current combined GLSL shader - `panopainter_validate_shaders` validates the current combined GLSL shader
files for one vertex stage marker, one fragment stage marker, valid marker files for one vertex stage marker, one fragment stage marker, valid marker
order, and existing relative includes. order, and existing relative includes.

View File

@@ -304,8 +304,9 @@ Goal: split libraries while keeping current app behavior.
Status: started. `pp_foundation` exists with binary stream utilities and Status: started. `pp_foundation` exists with binary stream utilities and
boundary/overread tests. It also owns strict decimal `uint32` parsing used by boundary/overread tests. It also owns strict decimal `uint32` parsing used by
`pano_cli`, with rejection tests for empty, signed, mixed, and overflowing `pano_cli`, with rejection tests for empty, signed, mixed, and overflowing
input. A deterministic `TraceRecorder` now records component/name/thread/frame input. A structured logging facade and deterministic `TraceRecorder` now
and stroke timing spans with invalid-end tests. `pp_assets` has started with record component/name/thread/frame/stroke metadata with filtering and invalid-end
tests. `pp_assets` has started with
PNG/JPEG signature detection plus PPI header recognition, with PNG/JPEG signature detection plus PPI header recognition, with
corrupt/truncated/unsupported tests. corrupt/truncated/unsupported tests.
`pp_paint` has started with CPU reference math for the five current shader `pp_paint` has started with CPU reference math for the five current shader
@@ -523,7 +524,7 @@ Last verified on 2026-06-01:
```powershell ```powershell
cmake --preset windows-msvc-default cmake --preset windows-msvc-default
cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter
ctest --preset desktop-fast --build-config Debug ctest --preset desktop-fast --build-config Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
@@ -537,6 +538,7 @@ powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -P
Results: Results:
- `pp_foundation_binary_stream_tests` passed. - `pp_foundation_binary_stream_tests` passed.
- `pp_foundation_log_tests` passed.
- `pp_foundation_parse_tests` passed. - `pp_foundation_parse_tests` passed.
- `pp_foundation_trace_tests` passed. - `pp_foundation_trace_tests` passed.
- `pp_assets_image_format_tests` passed. - `pp_assets_image_format_tests` passed.

View File

@@ -1,7 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[string[]]$Presets = @("android-arm64"), [string[]]$Presets = @("android-arm64"),
[string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_ppi_header_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests") [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_ppi_header_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests")
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"

View File

@@ -3,7 +3,7 @@ set -u
preset="${1:-android-arm64}" preset="${1:-android-arm64}"
shift || true shift || true
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}" targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}"
start="$(date +%s)" start="$(date +%s)"
cmake --preset "$preset" cmake --preset "$preset"

93
src/foundation/log.cpp Normal file
View File

@@ -0,0 +1,93 @@
#include "foundation/log.h"
namespace pp::foundation {
namespace {
[[nodiscard]] bool should_write(LogLevel level, LogLevel min_level) noexcept
{
return static_cast<std::uint8_t>(level) >= static_cast<std::uint8_t>(min_level);
}
}
Logger::Logger(ILogSink& sink) noexcept
: sink_(&sink)
{
}
void Logger::set_min_level(LogLevel level) noexcept
{
min_level_ = level;
}
LogLevel Logger::min_level() const noexcept
{
return min_level_;
}
Status Logger::write(
LogLevel level,
std::string_view component,
std::string_view message,
std::uint64_t frame_id,
std::uint64_t stroke_id,
std::uint64_t thread_id) noexcept
{
if (component.empty()) {
return Status::invalid_argument("log component must not be empty");
}
if (message.empty()) {
return Status::invalid_argument("log message must not be empty");
}
if (!should_write(level, min_level_)) {
return Status::success();
}
sink_->write(LogRecord {
.level = level,
.component = std::string(component),
.message = std::string(message),
.frame_id = frame_id,
.stroke_id = stroke_id,
.thread_id = thread_id,
});
return Status::success();
}
void MemoryLogSink::write(const LogRecord& record) noexcept
{
records_.push_back(record);
}
const std::vector<LogRecord>& MemoryLogSink::records() const noexcept
{
return records_;
}
void MemoryLogSink::clear() noexcept
{
records_.clear();
}
const char* log_level_name(LogLevel level) noexcept
{
switch (level) {
case LogLevel::trace:
return "trace";
case LogLevel::debug:
return "debug";
case LogLevel::info:
return "info";
case LogLevel::warning:
return "warning";
case LogLevel::error:
return "error";
}
return "unknown";
}
}

67
src/foundation/log.h Normal file
View File

@@ -0,0 +1,67 @@
#pragma once
#include "foundation/result.h"
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
namespace pp::foundation {
enum class LogLevel : std::uint8_t {
trace,
debug,
info,
warning,
error,
};
struct LogRecord {
LogLevel level = LogLevel::info;
std::string component;
std::string message;
std::uint64_t frame_id = 0;
std::uint64_t stroke_id = 0;
std::uint64_t thread_id = 0;
};
class ILogSink {
public:
virtual ~ILogSink() = default;
virtual void write(const LogRecord& record) noexcept = 0;
};
class Logger {
public:
explicit Logger(ILogSink& sink) noexcept;
void set_min_level(LogLevel level) noexcept;
[[nodiscard]] LogLevel min_level() const noexcept;
[[nodiscard]] Status write(
LogLevel level,
std::string_view component,
std::string_view message,
std::uint64_t frame_id = 0,
std::uint64_t stroke_id = 0,
std::uint64_t thread_id = 0) noexcept;
private:
ILogSink* sink_ = nullptr;
LogLevel min_level_ = LogLevel::trace;
};
class MemoryLogSink final : public ILogSink {
public:
void write(const LogRecord& record) noexcept override;
[[nodiscard]] const std::vector<LogRecord>& records() const noexcept;
void clear() noexcept;
private:
std::vector<LogRecord> records_;
};
[[nodiscard]] const char* log_level_name(LogLevel level) noexcept;
}

View File

@@ -16,6 +16,16 @@ add_test(NAME pp_foundation_binary_stream_tests COMMAND pp_foundation_binary_str
set_tests_properties(pp_foundation_binary_stream_tests PROPERTIES set_tests_properties(pp_foundation_binary_stream_tests PROPERTIES
LABELS "foundation;desktop-fast") LABELS "foundation;desktop-fast")
add_executable(pp_foundation_log_tests
foundation/log_tests.cpp)
target_link_libraries(pp_foundation_log_tests PRIVATE
pp_foundation
pp_test_harness)
add_test(NAME pp_foundation_log_tests COMMAND pp_foundation_log_tests)
set_tests_properties(pp_foundation_log_tests PROPERTIES
LABELS "foundation;desktop-fast")
add_executable(pp_foundation_parse_tests add_executable(pp_foundation_parse_tests
foundation/parse_tests.cpp) foundation/parse_tests.cpp)
target_link_libraries(pp_foundation_parse_tests PRIVATE target_link_libraries(pp_foundation_parse_tests PRIVATE

View File

@@ -0,0 +1,82 @@
#include "foundation/log.h"
#include "test_harness.h"
#include <string_view>
using pp::foundation::LogLevel;
using pp::foundation::Logger;
using pp::foundation::MemoryLogSink;
using pp::foundation::StatusCode;
using pp::foundation::log_level_name;
namespace {
void writes_structured_records(pp::tests::Harness& h)
{
MemoryLogSink sink;
Logger logger(sink);
const auto status = logger.write(LogLevel::info, "paint", "stroke committed", 7, 11, 3);
PP_EXPECT(h, status.ok());
PP_EXPECT(h, sink.records().size() == 1U);
PP_EXPECT(h, sink.records()[0].level == LogLevel::info);
PP_EXPECT(h, sink.records()[0].component == std::string_view("paint"));
PP_EXPECT(h, sink.records()[0].message == std::string_view("stroke committed"));
PP_EXPECT(h, sink.records()[0].frame_id == 7U);
PP_EXPECT(h, sink.records()[0].stroke_id == 11U);
PP_EXPECT(h, sink.records()[0].thread_id == 3U);
}
void filters_below_minimum_level(pp::tests::Harness& h)
{
MemoryLogSink sink;
Logger logger(sink);
logger.set_min_level(LogLevel::warning);
PP_EXPECT(h, logger.min_level() == LogLevel::warning);
PP_EXPECT(h, logger.write(LogLevel::debug, "ui", "layout pass").ok());
PP_EXPECT(h, logger.write(LogLevel::warning, "ui", "slow layout").ok());
PP_EXPECT(h, sink.records().size() == 1U);
PP_EXPECT(h, sink.records()[0].level == LogLevel::warning);
}
void rejects_empty_component_or_message(pp::tests::Harness& h)
{
MemoryLogSink sink;
Logger logger(sink);
const auto empty_component = logger.write(LogLevel::error, "", "message");
const auto empty_message = logger.write(LogLevel::error, "renderer", "");
PP_EXPECT(h, !empty_component.ok());
PP_EXPECT(h, empty_component.code == StatusCode::invalid_argument);
PP_EXPECT(h, !empty_message.ok());
PP_EXPECT(h, empty_message.code == StatusCode::invalid_argument);
PP_EXPECT(h, sink.records().empty());
}
void exposes_stable_level_names_and_clear(pp::tests::Harness& h)
{
MemoryLogSink sink;
Logger logger(sink);
PP_EXPECT(h, log_level_name(LogLevel::trace) == std::string_view("trace"));
PP_EXPECT(h, log_level_name(LogLevel::error) == std::string_view("error"));
PP_EXPECT(h, logger.write(LogLevel::info, "assets", "loaded").ok());
PP_EXPECT(h, sink.records().size() == 1U);
sink.clear();
PP_EXPECT(h, sink.records().empty());
}
}
int main()
{
pp::tests::Harness harness;
harness.run("writes_structured_records", writes_structured_records);
harness.run("filters_below_minimum_level", filters_below_minimum_level);
harness.run("rejects_empty_component_or_message", rejects_empty_component_or_message);
harness.run("exposes_stable_level_names_and_clear", exposes_stable_level_names_and_clear);
return harness.finish();
}