diff --git a/Cargo.lock b/Cargo.lock index 263fcbd1..1706fd27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2822,7 +2822,6 @@ dependencies = [ "iab_gpp", "jose-jwk", "log", - "log-fastly", "lol_html", "matchit", "pin-project-lite", diff --git a/crates/trusted-server-adapter-fastly/src/logging.rs b/crates/trusted-server-adapter-fastly/src/logging.rs new file mode 100644 index 00000000..3322677b --- /dev/null +++ b/crates/trusted-server-adapter-fastly/src/logging.rs @@ -0,0 +1,77 @@ +use chrono::{SecondsFormat, Utc}; +use log_fastly::Logger; + +/// Extracts the final `::` segment from a Rust module path for use as a log label. +/// +/// Falls back to the full target string when the input contains no separator or +/// when the separator appears at the trailing position (e.g. `"foo::"`), which +/// would otherwise produce an empty label in log output. +fn target_label(target: &str) -> &str { + match target.rsplit_once("::") { + Some((head, "")) => head, + Some((_, last)) => last, + None => target, + } +} + +/// Initialises the Fastly-backed `fern` logger and installs it as the global logger. +/// +/// Log records are forwarded to the `tslog` Fastly endpoint and echoed to stdout. +/// Each line is prefixed with an RFC 3339 timestamp, level, and the final segment +/// of the record's target module path. +/// +/// # Panics +/// +/// Panics if the logger cannot be built or if a global logger has already been set. +pub(crate) fn init_logger() { + let logger = Logger::builder() + .default_endpoint("tslog") + .echo_stdout(true) + .max_level(log::LevelFilter::Info) + .build() + .expect("should build Logger"); + + fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} {} [{}] {}", + Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true), + record.level(), + target_label(record.target()), + message + )); + }) + .chain(Box::new(logger) as Box) + .apply() + .expect("should initialize logger"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn target_label_extracts_correct_segment() { + assert_eq!( + target_label("trusted_server_adapter_fastly::proxy"), + "proxy", + "should handle standard single-separator case" + ); + assert_eq!( + target_label("foo::bar::baz"), + "baz", + "should handle multiple separators" + ); + assert_eq!( + target_label("no_separators_here"), + "no_separators_here", + "should handle inputs without ::" + ); + assert_eq!(target_label(""), "", "should handle empty strings"); + assert_eq!( + target_label("trailing::"), + "trailing", + "should strip separator when trailing segment is empty" + ); + } +} diff --git a/crates/trusted-server-adapter-fastly/src/main.rs b/crates/trusted-server-adapter-fastly/src/main.rs index 345a6bbf..6b81637a 100644 --- a/crates/trusted-server-adapter-fastly/src/main.rs +++ b/crates/trusted-server-adapter-fastly/src/main.rs @@ -1,7 +1,6 @@ use error_stack::Report; use fastly::http::Method; use fastly::{Error, Request, Response}; -use log_fastly::Logger; use trusted_server_core::auction::endpoints::handle_auction; use trusted_server_core::auction::{build_orchestrator, AuctionOrchestrator}; @@ -28,6 +27,7 @@ use trusted_server_core::settings::Settings; use trusted_server_core::settings_data::get_settings; mod error; +mod logging; mod management_api; mod platform; @@ -36,7 +36,7 @@ use crate::platform::{build_runtime_services, open_kv_store, UnavailableKvStore} #[fastly::main] fn main(req: Request) -> Result { - init_logger(); + logging::init_logger(); // Keep the health probe independent from settings loading and routing so // readiness checks still get a cheap liveness response during startup. @@ -241,30 +241,3 @@ fn finalize_response(settings: &Settings, geo_info: Option<&GeoInfo>, response: response.set_header(key, value); } } - -fn init_logger() { - let logger = Logger::builder() - .default_endpoint("tslog") - .echo_stdout(true) - .max_level(log::LevelFilter::Info) - .build() - .expect("should build Logger"); - - fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} {} [{}] {}", - chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true), - record.level(), - record - .target() - .split("::") - .last() - .unwrap_or(record.target()), - message - )) - }) - .chain(Box::new(logger) as Box) - .apply() - .expect("should initialize logger"); -} diff --git a/crates/trusted-server-core/Cargo.toml b/crates/trusted-server-core/Cargo.toml index 061c4d27..ddc8e2e2 100644 --- a/crates/trusted-server-core/Cargo.toml +++ b/crates/trusted-server-core/Cargo.toml @@ -33,7 +33,6 @@ iab_gpp = { workspace = true } jose-jwk = { workspace = true } log = { workspace = true } rand = { workspace = true } -log-fastly = { workspace = true } lol_html = { workspace = true } matchit = { workspace = true } pin-project-lite = { workspace = true } diff --git a/docs/superpowers/plans/2026-04-02-pr10-logging-initialization.md b/docs/superpowers/plans/2026-04-02-pr10-logging-initialization.md new file mode 100644 index 00000000..d699df60 --- /dev/null +++ b/docs/superpowers/plans/2026-04-02-pr10-logging-initialization.md @@ -0,0 +1,289 @@ +# PR 10 Logging Initialization Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Keep logging backend initialization adapter-owned by extracting Fastly logging setup into an adapter-local module and removing `log-fastly` from `trusted-server-core`. + +**Architecture:** `trusted-server-core` continues to emit logs only through `log` macros and has no platform logging backend dependency. `trusted-server-adapter-fastly` owns Fastly-specific logger initialization behind a local `logging.rs` module, and `main.rs` just calls into that adapter-local entrypoint. + +**Tech Stack:** Rust 2024 edition conventions, `log`, `log-fastly`, `fern`, `chrono` + +--- + +## File Structure + +- Create: `crates/trusted-server-adapter-fastly/src/logging.rs` + - Own Fastly-specific logger setup and any small formatting helpers needed for unit testing. +- Modify: `crates/trusted-server-adapter-fastly/src/main.rs` + - Stop carrying logger implementation details directly; import the adapter-local module and call `logging::init_logger()`. +- Modify: `crates/trusted-server-core/Cargo.toml` + - Remove `log-fastly` from core dependencies. +- Modify: `Cargo.lock` + - Lockfile update after dependency removal. + +The plan intentionally avoids any core logging trait or shared abstraction. Future adapters can mirror the same adapter-local module shape without forcing a premature common interface. + +## Tasks + +### Task 1: Extract Fastly logger helper and initializer into an adapter-local module + +**Files:** + +- Create: `crates/trusted-server-adapter-fastly/src/logging.rs` + +- [ ] **Step 1: Write a failing unit test for a non-allocating formatting helper** + +Create `crates/trusted-server-adapter-fastly/src/logging.rs` with a test-first skeleton. Add a helper test for the target-label extraction logic without trying to install a global logger: + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn target_label_uses_last_target_segment() { + assert_eq!( + target_label("trusted_server_adapter_fastly::proxy"), + "proxy", + "should use the final target segment" + ); + } +} +``` + +Also add a production skeleton so the file compiles but the test fails: + +```rust +fn target_label(target: &str) -> &str { + target +} +``` + +- [ ] **Step 2: Run the adapter test to verify it fails** + +Run: + +```bash +cargo test --package trusted-server-adapter-fastly logging -- --nocapture +``` + +Expected: FAIL because `target_label()` returns the full target instead of the final segment. + +- [ ] **Step 3: Implement the minimal helper and adapter logger initializer** + +Replace the skeleton with the real adapter-local module: + +```rust +use chrono::{SecondsFormat, Utc}; +use log_fastly::Logger; + +fn target_label(target: &str) -> &str { + match target.rsplit_once("::") { + Some((head, "")) => head, + Some((_, last)) => last, + None => target, + } +} + +pub(crate) fn init_logger() { + let logger = Logger::builder() + .default_endpoint("tslog") + .echo_stdout(true) + .max_level(log::LevelFilter::Info) + .build() + .expect("should build Logger"); + + fern::Dispatch::new() + .format(|out, _message, record| { + out.finish(format_args!( + "{} {} [{}] {}", + Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true), + record.level(), + target_label(record.target()), + record.args() + )); + }) + .chain(Box::new(logger) as Box) + .apply() + .expect("should initialize logger"); +} +``` + +Keep the logic semantically equivalent to the current `main.rs` formatting and avoid introducing a new heap allocation on each log call. + +- [ ] **Step 4: Run the adapter test to verify it passes** + +Run: + +```bash +cargo test --package trusted-server-adapter-fastly logging -- --nocapture +``` + +Expected: PASS. + +- [ ] **Step 5: Commit the extracted logging module** + +```bash +git add crates/trusted-server-adapter-fastly/src/logging.rs +git commit -m "Extract Fastly logging initialization into adapter module" +``` + +--- + +### Task 2: Wire `main.rs` to the adapter-local logging module + +**Files:** + +- Modify: `crates/trusted-server-adapter-fastly/src/main.rs` + +- [ ] **Step 1: Write a failing compile-time integration step for the new module wiring** + +Update `main.rs` to reference `logging::init_logger()` before the module exists in the file: + +```rust +mod logging; + +#[fastly::main] +fn main(req: Request) -> Result { + logging::init_logger(); + // ... +} +``` + +Delete the old inline `init_logger()` function and remove imports that only it used: + +- `use log_fastly::Logger;` +- any `chrono`/`fern` imports that are no longer needed in `main.rs` + +- [ ] **Step 2: Run the adapter package tests to verify the extraction is wired correctly** + +Run: + +```bash +cargo test --package trusted-server-adapter-fastly -- --nocapture +``` + +Expected: PASS. If compilation fails, fix the module imports and remaining references in `main.rs`. + +- [ ] **Step 3: Commit the adapter wiring cleanup** + +```bash +git add crates/trusted-server-adapter-fastly/src/main.rs +git commit -m "Wire Fastly main.rs to adapter-local logging module" +``` + +--- + +### Task 3: Remove `log-fastly` from core + +**Files:** + +- Modify: `crates/trusted-server-core/Cargo.toml` +- Modify: `Cargo.lock` + +- [ ] **Step 1: Verify core does not reference `log-fastly` directly** + +Run: + +```bash +rg -n "log_fastly|Logger::builder|Logger::from_env" crates/trusted-server-core +``` + +Expected: no matches. + +- [ ] **Step 2: Remove `log-fastly` from core dependencies** + +In `crates/trusted-server-core/Cargo.toml`, delete: + +```toml +log-fastly = { workspace = true } +``` + +Do not remove `log = { workspace = true }`. + +- [ ] **Step 3: Update the lockfile** + +Run: + +```bash +cargo test --package trusted-server-core --lib --no-run +``` + +Expected: `Cargo.lock` updates only as needed for the dependency graph while core still compiles. + +- [ ] **Step 4: Confirm `log-fastly` remains adapter-only** + +Run: + +```bash +rg -n "log-fastly" crates +``` + +Expected: match only in `crates/trusted-server-adapter-fastly/Cargo.toml`. + +- [ ] **Step 5: Commit the dependency cleanup** + +```bash +git add crates/trusted-server-core/Cargo.toml Cargo.lock +git commit -m "Remove log-fastly from trusted-server-core" +``` + +--- + +### Task 4: Run project verification gates + +**Files:** + +- Verify the whole workspace after the logging extraction and dependency cleanup + +- [ ] **Step 1: Format check** + +Run: + +```bash +cargo fmt --all -- --check +``` + +Expected: PASS. If it fails, run `cargo fmt --all` and re-run the check. + +- [ ] **Step 2: Clippy** + +Run: + +```bash +cargo clippy --workspace --all-targets --all-features -- -D warnings +``` + +Expected: PASS. + +- [ ] **Step 3: Full workspace tests** + +Run: + +```bash +cargo test --workspace +``` + +Expected: PASS. + +- [ ] **Step 4: Commit any formatting fallout** + +Only if `cargo fmt --all` changed files: + +```bash +git add -A +git commit -m "Fix formatting after logging extraction" +``` + +--- + +## Acceptance Checklist + +- [ ] `crates/trusted-server-adapter-fastly/src/logging.rs` exists +- [ ] `crates/trusted-server-adapter-fastly/src/main.rs` no longer contains the inline Fastly logger implementation +- [ ] `crates/trusted-server-core/Cargo.toml` no longer depends on `log-fastly` +- [ ] `rg -n "log-fastly" crates` reports only the Fastly adapter crate +- [ ] `trusted-server-core` still uses `log` macros and compiles without a Fastly-specific logging backend dependency +- [ ] `cargo fmt --all -- --check` passes +- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` passes +- [ ] `cargo test --workspace` passes diff --git a/docs/superpowers/specs/2026-04-02-pr10-logging-initialization-design.md b/docs/superpowers/specs/2026-04-02-pr10-logging-initialization-design.md new file mode 100644 index 00000000..4bf5af21 --- /dev/null +++ b/docs/superpowers/specs/2026-04-02-pr10-logging-initialization-design.md @@ -0,0 +1,69 @@ +# PR 10 Logging Initialization Design + +**Goal:** Keep logging backend initialization adapter-owned so `trusted-server-core` remains platform-agnostic while Fastly continues to initialize its own `log-fastly` backend. + +## Problem + +`trusted-server-core` still declares a `log-fastly` dependency even though log +backend setup already happens in the Fastly adapter entrypoint. That keeps a +Fastly-specific crate in the core dependency graph and weakens the migration +boundary needed for future EdgeZero adapters such as Cloudflare, Spin, and +Axum. + +## Design + +### Responsibility split + +- `trusted-server-core` emits logs only through `log` macros. +- Each adapter crate owns logger initialization and backend wiring. +- Fastly-specific logger setup moves behind an adapter-local module boundary. + +This keeps core free of platform logging backends while establishing a clean +pattern future adapters can mirror without forcing a shared abstraction too +early. + +### Fastly adapter shape + +Create an adapter-local logging module in +`crates/trusted-server-adapter-fastly/src/logging.rs` that exposes a small +`init_logger()` function. The implementation stays Fastly-specific and can keep +using `log-fastly`, `fern`, and the existing formatting choices. + +`crates/trusted-server-adapter-fastly/src/main.rs` should only import that +module and call `logging::init_logger()` during startup. + +### Core crate shape + +Remove `log-fastly` from +`crates/trusted-server-core/Cargo.toml`. No production code in core should +change unless compilation reveals an unexpected dependency. The intended end +state is that core depends on `log` only. + +## File impact + +- Create: `crates/trusted-server-adapter-fastly/src/logging.rs` +- Modify: `crates/trusted-server-adapter-fastly/src/main.rs` +- Modify: `crates/trusted-server-core/Cargo.toml` +- Modify: `Cargo.lock` + +## Testing and verification + +- Add or update small adapter-local tests only if needed for the extracted + logging module. +- Run the standard project gates: + - `cargo fmt --all -- --check` + - `cargo clippy --workspace --all-targets --all-features -- -D warnings` + - `cargo test --workspace` + +## Out of scope + +- Introducing a cross-adapter logging trait in core +- Changing log formatting semantics beyond what is needed to extract the module +- Adding logging implementations for non-Fastly adapters + +## Acceptance + +- `log-fastly` exists only in the Fastly adapter dependency graph +- Core uses `log` macros without any Fastly-specific logging backend dependency +- Fastly adapter still initializes logging at startup +- Workspace verification gates pass