Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 5 additions & 4 deletions crates/logfwd-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,9 @@ pub enum Format {
Raw,
Auto,
/// Human-readable colored console output for debugging/testing.
#[serde(alias = "text")]
Console,
/// Plain text output.
Text,
}

impl fmt::Display for Format {
Expand All @@ -203,6 +204,7 @@ impl fmt::Display for Format {
Format::Raw => f.write_str("raw"),
Format::Auto => f.write_str("auto"),
Format::Console => f.write_str("console"),
Format::Text => f.write_str("text"),
}
}
}
Expand Down Expand Up @@ -2437,9 +2439,8 @@ input:
output:
type: stdout
";
let cfg =
Config::load_str(yaml).expect("format: text should be accepted as alias for console");
let cfg = Config::load_str(yaml).expect("format: text should be accepted");
let pipe = &cfg.pipelines["default"];
assert_eq!(pipe.inputs[0].format, Some(Format::Console));
assert_eq!(pipe.inputs[0].format, Some(Format::Text));
}
Comment on lines 2441 to 2445

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test currently asserts pipe.inputs[0].format == Some(Format::Text), but the PR description is about stdout/output format handling. To actually validate the intended behavior, set output.format: text in the YAML and assert against pipe.outputs[0].format (and consider renaming the test since text is no longer an alias for console).

Copilot uses AI. Check for mistakes.
}
2 changes: 1 addition & 1 deletion crates/logfwd-output/src/arrow_ipc_sink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ mod tests {
]));
let message = StringArray::from(vec![Some("hello world"), Some("goodbye"), None]);
let status = Int64Array::from(vec![Some(200), Some(500), Some(404)]);
let latency = Float64Array::from(vec![Some(1.5), None, Some(3.14)]);
let latency = Float64Array::from(vec![Some(1.5), None, Some(3.5)]);
RecordBatch::try_new(
schema,
vec![Arc::new(message), Arc::new(status), Arc::new(latency)],
Expand Down
68 changes: 61 additions & 7 deletions crates/logfwd-output/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -757,9 +757,20 @@ pub fn build_sink_factory(
}
OutputType::Stdout => {
let fmt = match cfg.format.as_ref() {
Some(Format::Json) => StdoutFormat::Json,
Some(Format::Cri) | Some(Format::Raw) | Some(Format::Auto) => {
return Err(OutputError::Construction(format!(
"output '{name}': format '{}' is only supported for inputs",
cfg.format.as_ref().unwrap()
Comment on lines +760 to +763

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error branch uses cfg.format.as_ref().unwrap() in production code. It’s safe in this match arm, but it still introduces an unwrap() in a hot path/config path and can be avoided by binding the matched format (e.g. Some(f @ (Format::Cri | ...))) and formatting f directly.

Suggested change
Some(Format::Cri) | Some(Format::Raw) | Some(Format::Auto) => {
return Err(OutputError::Construction(format!(
"output '{name}': format '{}' is only supported for inputs",
cfg.format.as_ref().unwrap()
Some(f @ (Format::Cri | Format::Raw | Format::Auto)) => {
return Err(OutputError::Construction(format!(
"output '{name}': format '{f}' is only supported for inputs"

Copilot uses AI. Check for mistakes.
)));
}
Some(Format::Console) => StdoutFormat::Console,
_ => StdoutFormat::Text,
Some(Format::Text) => StdoutFormat::Text,
Some(Format::Json) | None => StdoutFormat::Json,
Some(f) => {
return Err(OutputError::Construction(format!(
"output '{name}': format '{f}' is not supported for stdout"
)));
}
};
Ok(Arc::new(StdoutSinkFactory::new(
name.to_string(),
Expand Down Expand Up @@ -1163,6 +1174,49 @@ mod tests {
assert_eq!(factory.name(), "test");
}

#[test]
fn test_build_sink_factory_stdout_input_only_formats() {
for format in [Format::Cri, Format::Raw, Format::Auto] {
let cfg = OutputConfig {
name: Some("test".to_string()),
output_type: OutputType::Stdout,
endpoint: None,
protocol: None,
compression: None,
request_mode: None,
format: Some(format.clone()),
path: None,
index: None,
auth: None,
};
let result = build_sink_factory("test", &cfg, Arc::new(ComponentStats::new()));
assert!(result.is_err(), "Expected error for format {:?}", format);
let err = result.err().unwrap();
assert!(
err.to_string().contains("only supported for inputs"),
"error should mention input-only format: {err}"
);
}
}

#[test]
fn test_build_sink_factory_stdout_default_format() {
let cfg = OutputConfig {
name: Some("test".to_string()),
output_type: OutputType::Stdout,
endpoint: None,
protocol: None,
compression: None,
request_mode: None,
format: None,
path: None,
index: None,
auth: None,
};
let factory = build_sink_factory("test", &cfg, Arc::new(ComponentStats::new())).unwrap();
assert_eq!(factory.name(), "test");
}
Comment on lines +1216 to +1218

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is intended to cover the new stdout default format behavior, but it only asserts the factory name. As written it won’t catch regressions where the default format changes back to text. Consider asserting the chosen format (e.g. by factoring the stdout-format selection into a small pure helper and unit-testing that, or otherwise making the selected StdoutFormat observable in tests).

Copilot uses AI. Check for mistakes.

#[test]
fn test_build_output_sink_otlp() {
let cfg = OutputConfig {
Expand Down Expand Up @@ -1807,17 +1861,17 @@ mod write_row_json_tests {
#[test]
fn float32_serializes_as_number() {
use arrow::array::Float32Array;
let batch = make_batch(vec![("val", Arc::new(Float32Array::from(vec![3.14_f32])))]);
let batch = make_batch(vec![("val", Arc::new(Float32Array::from(vec![3.5_f32])))]);
let json = render(&batch, 0);
let v: serde_json::Value = serde_json::from_str(&json).expect("must be valid JSON");
assert!(
v["val"].is_number(),
"Float32 should serialize as JSON number, got {json}"
);
let diff = (v["val"].as_f64().unwrap() - 3.14_f64).abs();
let diff = (v["val"].as_f64().unwrap() - 3.5_f64).abs();
assert!(
diff < 0.001,
"Float32 value should be ~3.14, got {}",
"Float32 value should be ~3.5, got {}",
v["val"]
);
}
Expand Down Expand Up @@ -1987,7 +2041,7 @@ mod write_row_json_tests {
let batch = make_batch(vec![
("dur_int", Arc::new(Int64Array::from(vec![42]))),
("label_str", Arc::new(StringArray::from(vec!["hello"]))),
("score_float", Arc::new(Float64Array::from(vec![3.14]))),
("score_float", Arc::new(Float64Array::from(vec![3.5]))),
]);
let json = render(&batch, 0);
let v: serde_json::Value = serde_json::from_str(&json).expect("must be valid JSON");
Expand All @@ -2001,7 +2055,7 @@ mod write_row_json_tests {
"alias 'label_str' must be preserved, got: {json}"
);
assert!(
(v["score_float"].as_f64().unwrap() - 3.14).abs() < 0.01,
(v["score_float"].as_f64().unwrap() - 3.5).abs() < 0.01,
"alias 'score_float' must be preserved, got: {json}"
);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/logfwd-output/src/sink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ mod tests {
Box::pin(async { Ok(()) })
}

fn name(&self) -> &str {
fn name(&self) -> &'static str {

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StubSink is implementing Sink::name(&self) -> &str, but this impl changes the return type to &'static str, which does not match the trait signature and will fail to compile. Please change it back to -> &str (you can still return a string literal).

Suggested change
fn name(&self) -> &'static str {
fn name(&self) -> &str {

Copilot uses AI. Check for mistakes.
"stub"
}

Expand Down
8 changes: 4 additions & 4 deletions crates/logfwd-output/tests/it/elasticsearch_arrow_ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async fn setup_test_data(sink: &mut Box<dyn logfwd_output::Sink>) -> RecordBatch
}

#[test]
#[ignore] // Run with: cargo test --test elasticsearch_arrow_ipc -- --ignored
#[ignore = "needs es instance"] // Run with: cargo test --test elasticsearch_arrow_ipc -- --ignored
fn test_query_arrow_all_documents() {
let rt = tokio::runtime::Runtime::new().expect("failed to create runtime");
rt.block_on(async {
Expand Down Expand Up @@ -188,7 +188,7 @@ fn test_query_arrow_all_documents() {
}

#[test]
#[ignore]
#[ignore = "needs es instance"]
fn test_query_arrow_with_filter() {
let rt = tokio::runtime::Runtime::new().expect("failed to create runtime");
rt.block_on(async {
Expand Down Expand Up @@ -249,7 +249,7 @@ fn test_query_arrow_with_filter() {
}

#[test]
#[ignore]
#[ignore = "needs es instance"]
fn test_query_arrow_with_projection() {
let rt = tokio::runtime::Runtime::new().expect("failed to create runtime");
rt.block_on(async {
Expand Down Expand Up @@ -318,7 +318,7 @@ fn test_query_arrow_with_projection() {
}

#[test]
#[ignore]
#[ignore = "needs es instance"]
fn test_query_arrow_empty_result() {
let rt = tokio::runtime::Runtime::new().expect("failed to create runtime");
rt.block_on(async {
Expand Down
Loading