diff --git a/src/core/log/fmt.rs b/src/core/log/fmt.rs
index 629449771e68587894d074ded25ad2e4597ead1b..8766eb8d5e01f45ed320f9295f0516a17539d5af 100644
--- a/src/core/log/fmt.rs
+++ b/src/core/log/fmt.rs
@@ -5,7 +5,7 @@
 
 pub fn html<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()>
 where
-	S: Write,
+	S: Write + ?Sized,
 {
 	let color = color::code_tag(level);
 	let level = level.as_str().to_uppercase();
@@ -19,7 +19,7 @@ pub fn html<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()>
 
 pub fn markdown<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()>
 where
-	S: Write,
+	S: Write + ?Sized,
 {
 	let level = level.as_str().to_uppercase();
 	writeln!(out, "`{level:>5}` `{span:^12}` `{msg}`")?;
@@ -29,7 +29,7 @@ pub fn markdown<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<
 
 pub fn markdown_table<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()>
 where
-	S: Write,
+	S: Write + ?Sized,
 {
 	let level = level.as_str().to_uppercase();
 	writeln!(out, "| {level:>5} | {span:^12} | {msg} |")?;
@@ -39,7 +39,7 @@ pub fn markdown_table<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> R
 
 pub fn markdown_table_head<S>(out: &mut S) -> Result<()>
 where
-	S: Write,
+	S: Write + ?Sized,
 {
 	write!(out, "| level | span | message |\n| ------: | :-----: | :------- |\n")?;
 
diff --git a/src/core/utils/string.rs b/src/core/utils/string.rs
index a597d198e533cd17b1a21b9c4b711074cb00ff1c..85282b30aa8b7e07468f31d1092ebb14ed6e4b03 100644
--- a/src/core/utils/string.rs
+++ b/src/core/utils/string.rs
@@ -30,6 +30,16 @@ macro_rules! is_format {
 	};
 }
 
+#[inline]
+pub fn collect_stream<F>(func: F) -> Result<String>
+where
+	F: FnOnce(&mut dyn std::fmt::Write) -> Result<()>,
+{
+	let mut out = String::new();
+	func(&mut out)?;
+	Ok(out)
+}
+
 #[inline]
 #[must_use]
 pub fn camel_to_snake_string(s: &str) -> String {