Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
611fab8
Add canary routing helpers: parse_rollout_pct, fnv1a_bucket, canary_r…
prk-Jr May 21, 2026
5718303
Add pinned FNV-1a test vector and debug_assert for canary bucket range
prk-Jr May 21, 2026
8ef14ec
Add edgezero_rollout_pct canary routing to Fastly entry point
prk-Jr May 21, 2026
8b8c303
Add edgezero_rollout_pct key to local Viceroy config store
prk-Jr May 21, 2026
76aee4a
Add EdgeZero canary rollout ops runbook
prk-Jr May 21, 2026
a1678dc
Warn when edgezero_rollout_pct key is absent (backward-compat full ro…
prk-Jr May 21, 2026
81d5179
Resolve review findings: canary preconditions, missing-IP log, runboo…
prk-Jr May 21, 2026
9c35fbf
Apply rustfmt formatting
prk-Jr May 21, 2026
951a35c
Merge branch 'feature/edgezero-pr19-spin-adapter' into feature/edgeze…
prk-Jr May 27, 2026
9743550
Demote absent edgezero_rollout_pct log to debug
prk-Jr Jun 13, 2026
7b420cc
Skip canary hashing for degenerate rollout and rename routing predicate
prk-Jr Jun 13, 2026
30002e2
Add unit tests for read_rollout_pct config-store branches
prk-Jr Jun 13, 2026
cbaa4a1
Clarify canary verification logs require debug-level logging
prk-Jr Jun 13, 2026
722c6dc
Extract canary dispatch decision into a testable helper
prk-Jr Jun 13, 2026
08168cb
Merge remote-tracking branch 'origin/feature/edgezero-pr19-spin-adapt…
prk-Jr Jun 13, 2026
3715cf5
Restore std::net::IpAddr import after merge with base
prk-Jr Jun 13, 2026
706b077
Make the canary runbook honest about production route observability
prk-Jr Jun 14, 2026
5f81e80
Make Fastly route-decision debug logs observable for local canary val…
prk-Jr Jun 15, 2026
17925a7
Make absent rollout_pct fail safe to legacy and fix canary docs
prk-Jr Jun 16, 2026
403de35
Align stale canary docs with fail-safe absent-key behavior
prk-Jr Jun 16, 2026
5afb4fe
Merge remote-tracking branch 'origin/feature/edgezero-pr19-spin-adapt…
prk-Jr Jun 20, 2026
848ca9c
Auto-raise Fastly log level under Viceroy via FASTLY_HOSTNAME
prk-Jr Jun 20, 2026
2e1575a
Filter route-decision debug logs at the outer fern Dispatch
prk-Jr Jun 20, 2026
9a83d17
Align canary docs with FASTLY_HOSTNAME log signal and fix-safe snippet
prk-Jr Jun 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion crates/trusted-server-adapter-fastly/src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,77 @@ fn target_label(target: &str) -> &str {
}
}

/// Environment variable that explicitly overrides the Fastly logger's maximum level.
///
/// Production ships at `Info`. `fastly compute serve`/Viceroy does not propagate
/// arbitrary shell environment variables into the Compute guest, so this override
/// only takes effect where the guest environment is populated deliberately (e.g.
/// the integration-test harness). Routine local validation does not depend on it —
/// see [`init_logger`], which auto-raises the level under Viceroy via the
/// [`LOCAL_HOSTNAME_ENV`] signal. The level stays at the safe default when this
/// variable is unset or unparseable.
const LOG_LEVEL_ENV: &str = "EDGEZERO_LOG_LEVEL";

/// Fastly-provided hostname environment variable, visible to guest code.
///
/// Viceroy (`fastly compute serve`) reports [`LOCAL_HOSTNAME`]; production cache
/// nodes report their real hostname. [`init_logger`] reads it to raise the log
/// level for local route-decision observability without affecting production.
///
/// Fastly-specific by design: this signal exists *because* Compute exposes a
/// guest-visible hostname that is fixed to `localhost` only under the simulator.
/// It must not be copied verbatim into the axum/spin/cloudflare adapters, where
/// it carries no such meaning and would mis-detect the runtime environment.
const LOCAL_HOSTNAME_ENV: &str = "FASTLY_HOSTNAME";

/// Hostname value Viceroy reports for [`LOCAL_HOSTNAME_ENV`] in local runs.
const LOCAL_HOSTNAME: &str = "localhost";

/// Resolves the logger's maximum level from an optional configured value,
/// falling back to `Info` when it is absent or not a recognised level filter.
fn resolve_max_level(configured: Option<&str>) -> log::LevelFilter {
configured
.and_then(|value| value.trim().parse::<log::LevelFilter>().ok())
.unwrap_or(log::LevelFilter::Info)
}

/// 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.
///
/// The maximum level defaults to `Info`. Under Viceroy (`fastly compute serve`)
/// it is auto-raised to `debug` so route-decision lines are observable locally,
/// detected via the [`LOCAL_HOSTNAME_ENV`] signal. An explicit [`LOG_LEVEL_ENV`]
/// value takes precedence where the guest environment is populated — the value is
/// used as-is, so `error`/`off` lowers it just as `debug` raises it; see
/// [`resolve_max_level`].
///
/// # Panics
///
/// Panics if the Fastly logger cannot be built or if the global logger has already
/// been set.
pub(crate) fn init_logger() {
let configured = std::env::var(LOG_LEVEL_ENV).ok().or_else(|| {
// Viceroy reports the guest host as `localhost`; production cache nodes
// report their real hostname. Raise to debug only in that local
// environment so route-decision lines surface under `fastly compute
// serve` while production stays at the safe `Info` default.
(std::env::var(LOCAL_HOSTNAME_ENV).as_deref() == Ok(LOCAL_HOSTNAME))
.then(|| "debug".to_owned())
});
let max_level = resolve_max_level(configured.as_deref());

let logger = Logger::builder()
.default_endpoint("tslog")
.echo_stdout(true)
.max_level(log::LevelFilter::Info)
.max_level(max_level)
.build()
.expect("should build Logger");

fern::Dispatch::new()
.level(max_level)
.format(|out, message, record| {
out.finish(format_args!(
"{} {} [{}] {}",
Expand Down Expand Up @@ -77,4 +129,36 @@ mod tests {
"should strip separator when trailing segment is empty"
);
}

#[test]
fn resolve_max_level_defaults_to_info_when_unset() {
assert_eq!(
resolve_max_level(None),
log::LevelFilter::Info,
"should default to Info when the override is unset"
);
}

#[test]
fn resolve_max_level_raises_to_debug_for_viceroy_validation() {
assert_eq!(
resolve_max_level(Some("debug")),
log::LevelFilter::Debug,
"should raise to Debug so route-decision lines are observable locally"
);
assert_eq!(
resolve_max_level(Some(" DEBUG ")),
log::LevelFilter::Debug,
"should accept case-insensitive, surrounding-whitespace values"
);
}

#[test]
fn resolve_max_level_falls_back_to_info_on_unrecognised_value() {
assert_eq!(
resolve_max_level(Some("not-a-level")),
log::LevelFilter::Info,
"should keep the safe Info default for an unparseable override"
);
}
}
Loading
Loading