Skip to content

Commit

Permalink
s/log/output for buildpack_output
Browse files Browse the repository at this point in the history
Uses consistent naming so that we distinguish between the general concept of a "log" which is not usually visible to the end user and the "output" which is the primary interface with our end user.

Leaves the `section_log` code and renames it `inline_output`
  • Loading branch information
schneems committed Jan 26, 2024
1 parent a4caf07 commit 671fb83
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 134 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ That's all we need! We can now move on to finally write some buildpack code!

### Writing the Buildpack

The buildpack we're writing will be very simple. We will just log a "Hello World" message during the build
The buildpack we're writing will be very simple. We will just output a "Hello World" message during the build
and set the default process type to a command that will also emit "Hello World" when the application image is run.
Examples of more complex buildpacks can be found in the [examples directory](https://github.com/heroku/libcnb.rs/tree/main/examples).

Expand Down
86 changes: 40 additions & 46 deletions libherokubuildpack/src/output/buildpack_output.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! # Build output logging
//! # Buildpack output
//!
//! Use the `BuildLog` to output structured text as a buildpack is executing
//! Use the [`BuildpackOutput`] to output structured text as a buildpack is executing
//!
//! ```
//! use libherokubuildpack::output::buildpack_output::BuildpackOutput;
Expand All @@ -15,9 +15,7 @@
//! output.finish();
//! ```
//!
//! To log inside of a layer see `section_log`.
//!
//! For usage details run `cargo run --bin print_style_guide`
//! To output inside of a layer see [`inline_output`].
use crate::output::style;
use std::fmt::Debug;
use std::io::Write;
Expand All @@ -33,7 +31,7 @@ pub struct BuildpackOutput<T, W: Debug> {
pub(crate) _state: T,
}

/// A bag of data passed throughout the lifecycle of a `BuildLog`
/// A bag of data passed throughout the lifecycle of a [`BuildpackOutput`]
#[derive(Debug)]
pub(crate) struct BuildData {
pub(crate) started: Instant,
Expand All @@ -47,9 +45,9 @@ impl Default for BuildData {
}
}

/// Various states for `BuildOutput` to contain
/// Various states for [`BuildpackOutput`] to contain
///
/// The `BuildLog` struct acts as a logging state machine. These structs
/// The [`BuildpackOutput`] struct acts as an output state machine. These structs
/// are meant to represent those states
pub(crate) mod state {
#[derive(Debug)]
Expand Down Expand Up @@ -114,8 +112,8 @@ where
self.io
}

pub fn announce(self) -> AnnounceLog<state::Started, W> {
AnnounceLog {
pub fn announce(self) -> Announce<state::Started, W> {
Announce {
io: self.io,
data: self.data,
_state: state::Started,
Expand All @@ -139,12 +137,12 @@ where
self
}

pub fn step_timed_stream(mut self, s: &str) -> StreamLog<W> {
pub fn step_timed_stream(mut self, s: &str) -> Stream<W> {
self.mut_step(s);

let started = Instant::now();
let arc_io = Arc::new(Mutex::new(self.io));
let mut stream = StreamLog {
let mut stream = Stream {
arc_io,
data: self.data,
started,
Expand All @@ -162,8 +160,8 @@ where
}
}

pub fn announce(self) -> AnnounceLog<state::Section, W> {
AnnounceLog {
pub fn announce(self) -> Announce<state::Section, W> {
Announce {
io: self.io,
data: self.data,
_state: state::Section,
Expand All @@ -174,7 +172,7 @@ where

// Store internal state, print leading character exactly once on warning or important
#[derive(Debug)]
pub struct AnnounceLog<T, W>
pub struct Announce<T, W>
where
W: Write + Send + Sync + Debug + 'static,
{
Expand All @@ -184,12 +182,12 @@ where
leader: Option<String>,
}

impl<T, W> AnnounceLog<T, W>
impl<T, W> Announce<T, W>
where
T: Debug,
W: Write + Send + Sync + Debug + 'static,
{
fn log_warning(&mut self, s: &str) {
fn shared_warning(&mut self, s: &str) {
if let Some(leader) = self.leader.take() {
write_now(&mut self.io, leader);
}
Expand All @@ -198,7 +196,7 @@ where
writeln_now(&mut self.io, "");
}

fn log_important(&mut self, s: &str) {
fn shared_important(&mut self, s: &str) {
if let Some(leader) = self.leader.take() {
write_now(&mut self.io, leader);
}
Expand All @@ -215,20 +213,20 @@ where
}
}

impl<W> AnnounceLog<state::Section, W>
impl<W> Announce<state::Section, W>
where
W: Write + Send + Sync + Debug + 'static,
{
#[must_use]
pub fn warning(mut self, s: &str) -> AnnounceLog<state::Section, W> {
self.log_warning(s);
pub fn warning(mut self, s: &str) -> Announce<state::Section, W> {
self.shared_warning(s);

self
}

#[must_use]
pub fn important(mut self, s: &str) -> AnnounceLog<state::Section, W> {
self.log_important(s);
pub fn important(mut self, s: &str) -> Announce<state::Section, W> {
self.shared_important(s);

self
}
Expand All @@ -242,19 +240,19 @@ where
}
}

impl<W> AnnounceLog<state::Started, W>
impl<W> Announce<state::Started, W>
where
W: Write + Send + Sync + Debug + 'static,
{
#[must_use]
pub fn warning(mut self, s: &str) -> AnnounceLog<state::Started, W> {
self.log_warning(s);
pub fn warning(mut self, s: &str) -> Announce<state::Started, W> {
self.shared_warning(s);
self
}

#[must_use]
pub fn important(mut self, s: &str) -> AnnounceLog<state::Started, W> {
self.log_important(s);
pub fn important(mut self, s: &str) -> Announce<state::Started, W> {
self.shared_important(s);
self
}

Expand All @@ -279,39 +277,39 @@ where
W: Write + Send + Sync + Debug + 'static,
{
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let mut io = self.arc.lock().expect("Logging mutex poisoned");
let mut io = self.arc.lock().expect("Output mutex poisoned");
io.write(buf)
}

fn flush(&mut self) -> std::io::Result<()> {
let mut io = self.arc.lock().expect("Logging mutex poisoned");
let mut io = self.arc.lock().expect("Output mutex poisoned");
io.flush()
}
}

/// Stream output to the user
///
/// Mostly used for logging a running command
/// Mostly used for ouputting a running command
#[derive(Debug)]
pub struct StreamLog<W> {
pub struct Stream<W> {
data: BuildData,
arc_io: Arc<Mutex<W>>,
started: Instant,
}

impl<W> StreamLog<W>
impl<W> Stream<W>
where
W: Write + Send + Sync + Debug + 'static,
{
fn start(&mut self) {
let mut guard = self.arc_io.lock().expect("Logging mutex poisoned");
let mut guard = self.arc_io.lock().expect("Output mutex poisoned");
let mut io = guard.by_ref();
// Newline before stream https://github.com/heroku/libcnb.rs/issues/582
writeln_now(&mut io, "");
}

/// Yield boxed writer that can be used for formatting and streaming contents
/// back to the logger.
/// back to the output.
pub fn io(&mut self) -> Box<dyn Write + Send + Sync> {
Box::new(crate::write::line_mapped(
LockedWriter {
Expand All @@ -338,7 +336,7 @@ where
let mut io = Arc::try_unwrap(self.arc_io)
.expect("Expected buildpack author to not retain any IO streaming IO instances")
.into_inner()
.expect("Logging mutex was poisioned");
.expect("Output mutex was poisioned");

// // Newline after stream
writeln_now(&mut io, "");
Expand All @@ -362,20 +360,16 @@ where
///
/// This is especially important for writing individual characters to the same line
fn write_now<D: Write>(destination: &mut D, msg: impl AsRef<str>) {
write!(destination, "{}", msg.as_ref()).expect("Logging error: UI writer closed");
write!(destination, "{}", msg.as_ref()).expect("Output error: UI writer closed");

destination
.flush()
.expect("Logging error: UI writer closed");
destination.flush().expect("Output error: UI writer closed");
}

/// Internal helper, ensures that all contents are always flushed (never buffered)
fn writeln_now<D: Write>(destination: &mut D, msg: impl AsRef<str>) {
writeln!(destination, "{}", msg.as_ref()).expect("Logging error: UI writer closed");
writeln!(destination, "{}", msg.as_ref()).expect("Output error: UI writer closed");

destination
.flush()
.expect("Logging error: UI writer closed");
destination.flush().expect("Output error: UI writer closed");
}

#[cfg(test)]
Expand Down Expand Up @@ -479,13 +473,13 @@ mod test {
#[test]
fn double_warning_step_padding() {
let writer = Vec::new();
let logger = BuildpackOutput::new(writer)
let output = BuildpackOutput::new(writer)
.start("RCT")
.section("Guest thoughts")
.step("The scenery here is wonderful")
.announce();

let io = logger
let io = output
.warning("It's too crowded here")
.warning("I'm tired")
.end_announce()
Expand Down
84 changes: 84 additions & 0 deletions libherokubuildpack/src/output/inline_output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//! Write to the build output in a [`BuildpackOutput`] format with functions
//!
//! ## What
//!
//! Outputting from within a layer can be difficult because calls to the layer interface are not
//! mutable nor consumable. Functions can be used at any time with no restrictions. The
//! only downside is that the buildpack author (you) is now responsible for:
//!
//! - Ensuring that [`BuildpackOutput::section`] funciton was called right before any of these
//! functions are called.
//! - Ensuring that you are not attempting to output while already streaming i.e. calling [`step`] within
//! a [`step_stream`] call.
//!
//! ## Use
//!
//! The main use case is writing output in a layer:
//!
//! ```no_run
//! use libherokubuildpack::output::inline_output;
//!
//! inline_output::step("Clearing the cache")
//! ```
use crate::output::buildpack_output::{state, BuildData, BuildpackOutput, Stream};
use std::io::Stdout;

/// Output a message as a single step, ideally a short message
///
/// ```
/// use libherokubuildpack::output::inline_output;
///
/// inline_output::step("Clearing cache (ruby version changed)");
/// ```
pub fn step(s: impl AsRef<str>) {
let _ = build_buildpack_output().step(s.as_ref());
}

/// Will print the input string and yield a [`Stream`] that can be used to print
/// to the output. The main use case is running commands
///
/// ```no_run
/// use fun_run::CommandWithName;
/// use libherokubuildpack::output::inline_output;
/// use libherokubuildpack::output::style;
///
/// let mut cmd = std::process::Command::new("bundle");
/// cmd.arg("install");
///
/// inline_output::step_stream(format!("Running {}", style::command(cmd.name())), |stream| {
/// cmd.stream_output(stream.io(), stream.io()).unwrap()
/// });
/// ```
///
/// Timing information will be output at the end of the step.
pub fn step_stream<T>(s: impl AsRef<str>, f: impl FnOnce(&mut Stream<Stdout>) -> T) -> T {
let mut stream = build_buildpack_output().step_timed_stream(s.as_ref());
let out = f(&mut stream);
let _ = stream.finish_timed_stream();
out
}

/// Print an error block to the output
pub fn error(s: impl AsRef<str>) {
build_buildpack_output().announce().error(s.as_ref());
}

/// Print an warning block to the output
pub fn warning(s: impl AsRef<str>) {
let _ = build_buildpack_output().announce().warning(s.as_ref());
}

/// Print an important block to the output
pub fn important(s: impl AsRef<str>) {
let _ = build_buildpack_output().announce().important(s.as_ref());
}

fn build_buildpack_output() -> BuildpackOutput<state::Section, Stdout> {
BuildpackOutput::<state::Section, Stdout> {
io: std::io::stdout(),
// Be careful not to do anything that might access this state
// as it's ephemeral data (i.e. not passed in from the start of the build)
data: BuildData::default(),
_state: state::Section,
}
}
1 change: 1 addition & 0 deletions libherokubuildpack/src/output/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod buildpack_output;
pub mod inline_output;
pub mod style;
mod util;
Loading

0 comments on commit 671fb83

Please sign in to comment.