Skip to content

Commit 24e0cc0

Browse files
committed
feat: add print command that outputs to logging system
1 parent b0eb852 commit 24e0cc0

File tree

7 files changed

+190
-16
lines changed

7 files changed

+190
-16
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,9 @@ $ http-nu --log-format jsonl :3001 '{|req| "hello"}'
326326

327327
Lifecycle events: `started`, `reloaded`, `stopping`, `stopped`, `stop_timed_out`
328328

329+
The `print` command outputs to the logging system (appears as `message: "print"`
330+
in JSONL).
331+
329332
### Trusted Proxies
330333

331334
When behind a reverse proxy, use `--trust-proxy` to extract client IP from

src/commands.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::logging::log_print;
12
use crate::response::{Response, ResponseBodyType};
23
use nu_engine::command_prelude::*;
34
use nu_protocol::{
@@ -1433,3 +1434,117 @@ impl Command for MdCommand {
14331434
.into_pipeline_data())
14341435
}
14351436
}
1437+
1438+
// === .print command ===
1439+
1440+
#[derive(Clone)]
1441+
pub struct PrintCommand;
1442+
1443+
impl Default for PrintCommand {
1444+
fn default() -> Self {
1445+
Self::new()
1446+
}
1447+
}
1448+
1449+
impl PrintCommand {
1450+
pub fn new() -> Self {
1451+
Self
1452+
}
1453+
}
1454+
1455+
impl Command for PrintCommand {
1456+
fn name(&self) -> &str {
1457+
"print"
1458+
}
1459+
1460+
fn description(&self) -> &str {
1461+
"Print the given values to the http-nu logging system."
1462+
}
1463+
1464+
fn extra_description(&self) -> &str {
1465+
r#"This command outputs to http-nu's logging system rather than stdout/stderr.
1466+
Messages appear in both human-readable and JSONL output modes.
1467+
1468+
`print` may be used inside blocks of code (e.g.: hooks) to display text during execution without interfering with the pipeline."#
1469+
}
1470+
1471+
fn search_terms(&self) -> Vec<&str> {
1472+
vec!["display"]
1473+
}
1474+
1475+
fn signature(&self) -> Signature {
1476+
Signature::build("print")
1477+
.input_output_types(vec![
1478+
(Type::Nothing, Type::Nothing),
1479+
(Type::Any, Type::Nothing),
1480+
])
1481+
.allow_variants_without_examples(true)
1482+
.rest("rest", SyntaxShape::Any, "the values to print")
1483+
.switch(
1484+
"no-newline",
1485+
"print without inserting a newline for the line ending",
1486+
Some('n'),
1487+
)
1488+
.switch("stderr", "print to stderr instead of stdout", Some('e'))
1489+
.switch(
1490+
"raw",
1491+
"print without formatting (including binary data)",
1492+
Some('r'),
1493+
)
1494+
.category(Category::Strings)
1495+
}
1496+
1497+
fn run(
1498+
&self,
1499+
engine_state: &EngineState,
1500+
stack: &mut Stack,
1501+
call: &Call,
1502+
input: PipelineData,
1503+
) -> Result<PipelineData, ShellError> {
1504+
let args: Vec<Value> = call.rest(engine_state, stack, 0)?;
1505+
let no_newline = call.has_flag(engine_state, stack, "no-newline")?;
1506+
let config = stack.get_config(engine_state);
1507+
1508+
let format_value = |val: &Value| -> String { val.to_expanded_string(" ", &config) };
1509+
1510+
if !args.is_empty() {
1511+
let messages: Vec<String> = args.iter().map(format_value).collect();
1512+
let message = if no_newline {
1513+
messages.join("")
1514+
} else {
1515+
messages.join("\n")
1516+
};
1517+
log_print(&message);
1518+
} else if !input.is_nothing() {
1519+
let message = match input {
1520+
PipelineData::Value(val, _) => format_value(&val),
1521+
PipelineData::ListStream(stream, _) => {
1522+
let vals: Vec<String> = stream.into_iter().map(|v| format_value(&v)).collect();
1523+
vals.join("\n")
1524+
}
1525+
PipelineData::ByteStream(stream, _) => stream.into_string()?,
1526+
PipelineData::Empty => String::new(),
1527+
};
1528+
if !message.is_empty() {
1529+
log_print(&message);
1530+
}
1531+
}
1532+
1533+
Ok(PipelineData::empty())
1534+
}
1535+
1536+
fn examples(&self) -> Vec<Example<'_>> {
1537+
vec![
1538+
Example {
1539+
description: "Print 'hello world'",
1540+
example: r#"print "hello world""#,
1541+
result: None,
1542+
},
1543+
Example {
1544+
description: "Print the sum of 2 and 3",
1545+
example: r#"print (2 + 3)"#,
1546+
result: None,
1547+
},
1548+
]
1549+
}
1550+
}

src/engine.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ use nu_protocol::{
1818

1919
use crate::commands::{
2020
HighlightCommand, HighlightLangCommand, HighlightThemeCommand, MdCommand, MjCommand,
21-
MjCompileCommand, MjRenderCommand, ResponseStartCommand, ReverseProxyCommand, StaticCommand,
22-
ToSse,
21+
MjCompileCommand, MjRenderCommand, PrintCommand, ResponseStartCommand, ReverseProxyCommand,
22+
StaticCommand, ToSse,
2323
};
2424
use crate::logging::log_error;
2525
use crate::stdlib::load_http_nu_stdlib;
@@ -302,6 +302,7 @@ impl Engine {
302302
Box::new(HighlightThemeCommand::new()),
303303
Box::new(HighlightLangCommand::new()),
304304
Box::new(MdCommand::new()),
305+
Box::new(PrintCommand::new()),
305306
])
306307
}
307308

src/logging.rs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,15 @@ pub enum Event {
112112
Error {
113113
error: String,
114114
},
115+
Print {
116+
message: String,
117+
},
115118
Stopping {
116119
inflight: usize,
117120
},
118121
Stopped,
119122
StopTimedOut,
123+
Shutdown,
120124
}
121125

122126
// --- Broadcast channel ---
@@ -192,6 +196,12 @@ pub fn log_error(error: &str) {
192196
});
193197
}
194198

199+
pub fn log_print(message: &str) {
200+
emit(Event::Print {
201+
message: message.to_string(),
202+
});
203+
}
204+
195205
pub fn log_stopping(inflight: usize) {
196206
emit(Event::Stopping { inflight });
197207
}
@@ -204,9 +214,13 @@ pub fn log_stop_timed_out() {
204214
emit(Event::StopTimedOut);
205215
}
206216

217+
pub fn shutdown() {
218+
emit(Event::Shutdown);
219+
}
220+
207221
// --- JSONL handler (dedicated writer thread does serialization + IO) ---
208222

209-
pub fn run_jsonl_handler(rx: broadcast::Receiver<Event>) {
223+
pub fn run_jsonl_handler(rx: broadcast::Receiver<Event>) -> std::thread::JoinHandle<()> {
210224
use std::io::Write;
211225

212226
std::thread::spawn(move || {
@@ -231,13 +245,19 @@ pub fn run_jsonl_handler(rx: broadcast::Receiver<Event>) {
231245
Err(broadcast::error::RecvError::Closed) => break,
232246
};
233247

248+
if matches!(event, Event::Shutdown) {
249+
let _ = stdout.flush();
250+
break;
251+
}
252+
234253
let needs_flush = matches!(
235254
&event,
236255
Event::Started { .. }
237256
| Event::Stopped
238257
| Event::StopTimedOut
239258
| Event::Reloaded
240259
| Event::Error { .. }
260+
| Event::Print { .. }
241261
);
242262

243263
let stamp = scru128::new().to_string();
@@ -309,6 +329,13 @@ pub fn run_jsonl_handler(rx: broadcast::Receiver<Event>) {
309329
"error": error,
310330
})
311331
}
332+
Event::Print { message } => {
333+
serde_json::json!({
334+
"stamp": stamp,
335+
"message": "print",
336+
"content": message,
337+
})
338+
}
312339
Event::Stopping { inflight } => {
313340
serde_json::json!({
314341
"stamp": stamp,
@@ -328,6 +355,7 @@ pub fn run_jsonl_handler(rx: broadcast::Receiver<Event>) {
328355
"message": "stop_timed_out",
329356
})
330357
}
358+
Event::Shutdown => unreachable!(),
331359
};
332360

333361
if let Ok(line) = serde_json::to_string(&json) {
@@ -341,7 +369,7 @@ pub fn run_jsonl_handler(rx: broadcast::Receiver<Event>) {
341369
}
342370

343371
let _ = stdout.flush();
344-
});
372+
})
345373
}
346374

347375
// --- Human-readable handler (dedicated thread) ---
@@ -445,7 +473,7 @@ fn format_complete_line(state: &RequestState, duration_ms: u64, bytes: u64) -> S
445473
)
446474
}
447475

448-
pub fn run_human_handler(rx: broadcast::Receiver<Event>) {
476+
pub fn run_human_handler(rx: broadcast::Receiver<Event>) -> std::thread::JoinHandle<()> {
449477
std::thread::spawn(move || {
450478
let mut rx = rx;
451479
let mut zone = ActiveZone::new();
@@ -498,6 +526,10 @@ pub fn run_human_handler(rx: broadcast::Receiver<Event>) {
498526
eprintln!("ERROR: {error}");
499527
zone.redraw(&active_ids, &requests);
500528
}
529+
Event::Print { message } => {
530+
zone.print_permanent(&format!("PRINT: {message}"));
531+
zone.redraw(&active_ids, &requests);
532+
}
501533
Event::Stopping { inflight } => {
502534
zone.print_permanent(&format!(
503535
"stopping, {inflight} connection(s) in flight..."
@@ -567,6 +599,7 @@ pub fn run_human_handler(rx: broadcast::Receiver<Event>) {
567599
zone.redraw(&active_ids, &requests);
568600
}
569601
}
602+
Event::Shutdown => break,
570603
}
571604
}
572605

@@ -578,7 +611,7 @@ pub fn run_human_handler(rx: broadcast::Receiver<Event>) {
578611
if lagged > 0 {
579612
println!("⚠ total lagged: {lagged} events dropped");
580613
}
581-
});
614+
})
582615
}
583616

584617
// --- RequestGuard: ensures Complete fires even on abort ---

src/main.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use http_nu::{
1414
listener::TlsConfig,
1515
logging::{
1616
init_broadcast, log_reloaded, log_started, log_stop_timed_out, log_stopped, log_stopping,
17-
run_human_handler, run_jsonl_handler,
17+
run_human_handler, run_jsonl_handler, shutdown,
1818
},
1919
Engine, Listener,
2020
};
@@ -457,10 +457,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
457457

458458
// Set up logging handler based on log format (both spawn dedicated threads)
459459
let rx = init_broadcast();
460-
match args.log_format {
460+
let log_handle = match args.log_format {
461461
LogFormat::Human => run_human_handler(rx),
462462
LogFormat::Jsonl => run_jsonl_handler(rx),
463-
}
463+
};
464464

465465
rustls::crypto::aws_lc_rs::default_provider()
466466
.install_default()
@@ -505,16 +505,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
505505

506506
engine.set_signals(interrupt.clone());
507507

508-
match engine.eval(&script) {
508+
let exit_code = match engine.eval(&script) {
509509
Ok(value) => {
510-
println!("{}", value.to_expanded_string(" ", &engine.state.config));
511-
return Ok(());
510+
let output = value.to_expanded_string(" ", &engine.state.config);
511+
if !output.is_empty() {
512+
println!("{output}");
513+
}
514+
0
512515
}
513516
Err(e) => {
514517
eprintln!("{e}");
515-
std::process::exit(1);
518+
1
516519
}
517-
}
520+
};
521+
shutdown();
522+
log_handle.join().ok();
523+
std::process::exit(exit_code);
518524
}
519525

520526
// Server mode (default)
@@ -643,5 +649,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
643649
args.trust_proxies,
644650
std::time::Instant::now(),
645651
)
646-
.await
652+
.await?;
653+
654+
shutdown();
655+
log_handle.join().ok();
656+
Ok(())
647657
}

src/test_handler.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use http_body_util::{BodyExt, Empty, Full};
66
use hyper::{body::Bytes, Request};
77
use tokio::time::Duration;
88

9-
use crate::commands::{MjCommand, ResponseStartCommand, StaticCommand, ToSse};
9+
use crate::commands::{MjCommand, PrintCommand, ResponseStartCommand, StaticCommand, ToSse};
1010
use crate::handler::handle;
1111

1212
fn no_trusted_proxies() -> Arc<Vec<ipnet::IpNet>> {
@@ -323,6 +323,7 @@ fn test_engine(script: &str) -> crate::Engine {
323323
Box::new(StaticCommand::new()),
324324
Box::new(ToSse {}),
325325
Box::new(MjCommand::new()),
326+
Box::new(PrintCommand::new()),
326327
])
327328
.unwrap();
328329
engine.parse_closure(script).unwrap();

tests/eval_test.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ fn test_eval_mj_compile_and_render() {
6161
.stdout("Hi World\n");
6262
}
6363

64+
#[test]
65+
fn test_eval_print() {
66+
Command::cargo_bin("http-nu")
67+
.unwrap()
68+
.args(["--log-format", "jsonl", "eval", "-c", r#"print "hello""#])
69+
.assert()
70+
.success()
71+
.stdout(predicates::str::contains(r#""message":"print""#))
72+
.stdout(predicates::str::contains(r#""content":"hello""#));
73+
}
74+
6475
#[test]
6576
fn test_eval_syntax_error() {
6677
Command::cargo_bin("http-nu")

0 commit comments

Comments
 (0)