Skip to content

Follow logs in tower cloud#73

Merged
bradhe merged 18 commits intodevelopfrom
feature/follow-logs-in-tower-cloud
Jul 28, 2025
Merged

Follow logs in tower cloud#73
bradhe merged 18 commits intodevelopfrom
feature/follow-logs-in-tower-cloud

Conversation

@bradhe
Copy link
Contributor

@bradhe bradhe commented Jul 27, 2025

This PR introduces the ability to follow Tower runs in the Tower cloud when you invoke them locally. The behavior by default is such that when you to a tower run the CLI will follow the logs. You can run tower run --detach to have it run locally, and you'll get a link to the Tower UI back.

You can see a demo here: https://asciinema.org/a/PWMkIRy6Igl3FeY3hzygGQg55

Note: I also relaxed the constraints of the CLI a little bit such that it can handle enums that don't match spec.

This comment was marked as outdated.

bradhe and others added 6 commits July 28, 2025 11:01
- Remove boxed models, since that's more what we expect.
- For detached runs, make sure all that works.
- For attached runs, print messages that are useful at the end.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
while let Some(event) = source.next().await {
match event {
Ok(reqwest_eventsource::Event::Open) => {
// TODO: This shouldn't happen.
Copy link
Contributor

Choose a reason for hiding this comment

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

these sorts of comments just tempt fate

Copy link
Contributor Author

Choose a reason for hiding this comment

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

correct--good point :) I added a panic.

let api_config: configuration::Configuration = config.into();

// These represent the messages that we'll stream to the client.
let (tx, rx) = mpsc::channel(1);
Copy link
Contributor

Choose a reason for hiding this comment

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

tx = transmit?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, tx = transmit, rx = receive. Convention from the Tokio docs.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ha! I commented the same. Very weird naming

let dt: DateTime<Utc> = DateTime::parse_from_rfc3339(&line.timestamp)
.unwrap()
.with_timezone(&Utc);
let ts = dt.format("%Y-%m-%d %H:%M:%S").to_string();
Copy link
Contributor

Choose a reason for hiding this comment

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

same as "%F %T" btw

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah nice. I'll update this. Actually, I'll refactor it out, too, to make it consistent.

}
}

impl<'de> Deserialize<'de> for {{{classname}}} {
Copy link
Contributor

Choose a reason for hiding this comment

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

What's going on here? How come all the auto generated code now derives Deserializer as well as Deserialize?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a bit hard to follow since it's in this (very hairy) mustache template. For enum types (with name {{{classname}}}), we implement the Deserialize trait. It takes an argument D which implements the Deserializer trait.

All of this is meant to allow us to deserialize enum types in a case-insensitive way! So both "PROGRAM" and "program" can deserialize to Channel::Program. It's otherwise very strict and often breaks out API when we don't conform to the spec--which, actually, was exactly what was happening for log lines and was blocking work on this PR!

let api_config: configuration::Configuration = config.into();

// These represent the messages that we'll stream to the client.
let (tx, rx) = mpsc::channel(1);
Copy link
Contributor

Choose a reason for hiding this comment

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

Ha! I commented the same. Very weird naming

@bradhe bradhe requested a review from Copilot July 28, 2025 16:42
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces the ability to follow Tower runs in real-time from the CLI and includes generated API model updates. The default behavior now follows run logs when executing tower run, with an optional --detach flag to run without following. Additionally, the Rust client generator configuration has been updated to handle enum deserialization more flexibly.

Key changes include:

  • Added real-time run following functionality with log streaming and completion monitoring
  • Added --detach flag to disable log following behavior
  • Updated generated API models to handle case-insensitive enum deserialization
  • Enhanced error handling and Ctrl+C interrupt support

Reviewed Changes

Copilot reviewed 172 out of 173 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
crates/tower-cmd/src/run.rs Main implementation of run following functionality with log streaming and completion monitoring
crates/tower-cmd/src/api.rs Added event streaming support and new API endpoints for run monitoring
crates/tower-cmd/src/util/dates.rs New utility module for consistent timestamp formatting
scripts/rust-client-templates/model.mustache Updated enum deserialization to be case-insensitive and more flexible
crates/tower-api/src/models/*.rs Generated API model updates with improved enum handling and removed boxed types
Comments suppressed due to low confidence (1)

crates/tower-cmd/src/run.rs:77

  • The match pattern uses Ok(Self::{{{name}}}) in the template but here it's missing the Ok() wrapper. This should be Ok(Self::Scheduled) to maintain consistency with the deserialization pattern.
            );

while let Some(event) = source.next().await {
match event {
Ok(reqwest_eventsource::Event::Open) => {
panic!("Received unexpected open event in log stream. This shouldn't happen.");
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

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

Using panic! in production code should be avoided. Consider returning an error instead of panicking, as this could crash the entire application unexpectedly.

Suggested change
panic!("Received unexpected open event in log stream. This shouldn't happen.");
debug!("Received unexpected open event in log stream. This shouldn't happen.");
break; // Exit the loop gracefully

Copilot uses AI. Check for mistakes.
},
Ok(Event::Message(message)) => {
// This is a bug in the program and should never happen.
panic!("Received message when expected an open event. Message: {:?}", message);
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

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

Using panic! in production code should be avoided. Consider returning an error instead of panicking to maintain application stability.

Copilot uses AI. Check for mistakes.
Comment on lines +556 to +559
let run = wait_for_run_completion(&config_clone, &run_clone).
await.
unwrap();

Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

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

Using unwrap() in async code can cause panics. Consider proper error handling and propagating the error to the caller instead of unwrapping.

Suggested change
let run = wait_for_run_completion(&config_clone, &run_clone).
await.
unwrap();
match wait_for_run_completion(&config_clone, &run_clone).await {
Ok(run) => {
let _ = tx.send(run);
}
Err(e) => {
eprintln!("Error waiting for run completion: {:?}", e);
}
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@konstantinoscs konstantinoscs Jul 28, 2025

Choose a reason for hiding this comment

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

Is this relevant?
Edit: I mean the copilot comment at the same line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we want it to panic in this case. The CLI will just die.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

@konstantinoscs konstantinoscs left a comment

Choose a reason for hiding this comment

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

Ok with me but I would prefer it if @socksy was also satisfied before we merge this

Comment on lines +556 to +559
let run = wait_for_run_completion(&config_clone, &run_clone).
await.
unwrap();

Copy link
Contributor

@konstantinoscs konstantinoscs Jul 28, 2025

Choose a reason for hiding this comment

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

Is this relevant?
Edit: I mean the copilot comment at the same line

@bradhe bradhe merged commit 345846b into develop Jul 28, 2025
4 checks passed
@bradhe bradhe deleted the feature/follow-logs-in-tower-cloud branch July 28, 2025 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants