///|
/// Options for `docker logs`.
pub(all) struct LogOptions {
/// Only the last N lines (`--tail`).
tail : Int32?
/// Prefix each line with an RFC3339Nano timestamp (`--timestamps`).
timestamps : Bool
/// Show logs since a timestamp or relative duration (`--since`).
since : String?
/// Show logs before a timestamp or relative duration (`--until`).
until : String?
} derive(Debug, Eq)
///|
pub extend LogOptions with Eq::{equal, not_equal}
///|
pub extend LogOptions with Debug::{to_repr}
///|
/// Default options: the whole log, no timestamps. Override any field with a
/// labeled argument, e.g. `LogOptions::new(tail=50, timestamps=true)`.
pub fn LogOptions::new(
tail? : Int32,
timestamps~ : Bool = false,
since? : String,
until? : String,
) -> LogOptions {
{ tail, timestamps, since, until, }
}
///|
/// `docker logs` arguments for the given options.
fn logs_args(container_id : String, options : LogOptions) -> Array[String] {
let args : Array[String] = ["logs"]
if options.timestamps {
args.push("--timestamps")
}
match options.tail {
Some(lines) => {
args.push("--tail")
args.push(lines.to_string())
}
None => ()
}
match options.since {
Some(timestamp) => {
args.push("--since")
args.push(timestamp)
}
None => ()
}
match options.until {
Some(timestamp) => {
args.push("--until")
args.push(timestamp)
}
None => ()
}
args.push(container_id)
args
}
///|
/// Container logs, stdout and stderr combined.
///
/// Docker keeps the two streams apart; they are concatenated here (stdout
/// first). Use `logs_split` when the distinction matters. For readiness checks
/// this is fine because application logs usually go to stderr.
pub async fn Container::logs(
self : Container,
options? : LogOptions,
) -> String raise TestContainerError {
let options = match options {
Some(options) => options
None => LogOptions::new()
}
let split = self.logs_split(options~)
combine_logs(split.stdout, split.stderr)
}
///|
/// Container logs with stdout and stderr kept apart.
pub async fn Container::logs_split(
self : Container,
options? : LogOptions,
) -> LogOutput raise TestContainerError {
let options = match options {
Some(options) => options
None => LogOptions::new()
}
let output = docker_command(logs_args(self.id, options))
if output.code != 0 {
raise DockerCommandFailed(
command=render_command("docker", logs_args(self.id, options)),
code=output.code,
stderr=command_error_text(output),
)
}
{ stdout: output.stdout, stderr: output.stderr, }
}
///|
pub(all) struct LogOutput {
stdout : String
stderr : String
} derive(Debug, Eq)
///|
pub extend LogOutput with Eq::{equal, not_equal}
///|
pub extend LogOutput with Debug::{to_repr}
///|
fn combine_logs(stdout : String, stderr : String) -> String {
let stdout = stdout.trim_end().to_owned()
let stderr = stderr.trim_end().to_owned()
if stdout == "" {
stderr
} else if stderr == "" {
stdout
} else {
stdout + "\n" + stderr
}
}
///|
/// Stream `docker logs --follow` line by line until the container exits or
/// `f` raises.
///
/// The callback receives each line without its trailing newline.
pub async fn Container::stream_logs(
self : Container,
f : (String) -> Unit raise,
) -> Unit {
@async.with_task_group(group => {
let (reader, writer) = @process.read_from_process() catch {
e => raise DockerNotAvailable(e.to_string())
}
defer reader.close()
let _ = @process.spawn(
group,
"docker",
["logs", "--follow", "--tail", "0", self.id],
stdout=writer,
stderr=writer,
) catch {
e => raise DockerNotAvailable(e.to_string())
}
defer writer.close()
while true {
match reader.read_until("\n") {
Some(line) => f(line)
None => break
}
}
})
}
///|
/// Last `lines` lines of the logs, or an empty string when they cannot be read.
/// Used to enrich error messages; never fails.
async fn Container::logs_for_error(self : Container) -> String {
let result : Result[String, TestContainerError] = Ok(
self.logs(options=LogOptions::new(tail=200)),
) catch {
e => Err(e)
}
match result {
Ok(logs) => logs
Err(_) => ""
}
}