Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: emit Docker and Shell logging to ProgressListener #221

Closed
wants to merge 21 commits into from

Conversation

HBobertz
Copy link
Contributor

@HBobertz HBobertz commented Dec 3, 2024

Related to #196

There is still a quiet flag however I will do that in another pr.

This change adds a new GlobalOutputHandler which as the name implies, is a global object which will send messages to the progress listener. If invoked from the CLI, the CLI first configures the progress as a ConsoleProgress() listener which prints to stderr by default. If invoked from AssetPublishing (the class which we programmatically interact with from the aws-cdk), then the globalOutputHandler will instead push the messages to the progress listener defined in that class.

This enables us to capture logging from anywhere and emit it to the configured pluggable progress listener.

@HBobertz HBobertz marked this pull request as ready for review December 8, 2024 22:14
lib/progress.ts Outdated Show resolved Hide resolved
test/mock-child_process.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@mrgrain mrgrain left a comment

Choose a reason for hiding this comment

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

Please describe what this PR is addressing and how its solving it. If there are follow-up PRs, describe in a bit more detail how they will address it.

Please describe how you've verified the changes.

test/mock-child_process.ts Outdated Show resolved Hide resolved
bin/logging.ts Outdated Show resolved Hide resolved
bin/logging.ts Outdated Show resolved Hide resolved
@mrgrain
Copy link
Contributor

mrgrain commented Dec 10, 2024

Fixes #196

BTW, if it doesn't close the issue, e.g. because a second PR is needed, you cannot put that in like that.

@HBobertz HBobertz changed the title chore: Modify logging to ensure docker and shell logging is properly emitted to pluggble log publisher chore: Ensure docker and shell logging is properly emitted to log publisher Dec 13, 2024
@HBobertz HBobertz changed the title chore: Ensure docker and shell logging is properly emitted to log publisher chore: Emit Docker and Shell logging to ProgressListener Dec 13, 2024
@HBobertz HBobertz changed the title chore: Emit Docker and Shell logging to ProgressListener chore: emit Docker and Shell logging to ProgressListener Dec 27, 2024
github-merge-queue bot pushed a commit that referenced this pull request Dec 30, 2024
Adding some additional tests, so I can verify #221 isn't causing any
breaking changes

These are LLM generated btw

---------

Signed-off-by: github-actions <[email protected]>
Co-authored-by: github-actions <[email protected]>
@HBobertz HBobertz requested a review from mrgrain December 30, 2024 16:16
Copy link
Contributor

@iliapolo iliapolo left a comment

Choose a reason for hiding this comment

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

I think we should consider a different approach for this PR. As I understand it, all we're after is to move away from code that directly prints to the screen.

We already have the IPublishProgressListener interface for that, can we not just make sure this listener is being passed to whichever code that needs it? Yeah its a little bit more work but I think avoids some functional complications as mentioned in the comments. Introducing yet another "emitter" makes the code even more convoluted than it is now. We will have:

  • IPublishProgressListener
  • GlobalOutputHandler
  • IHandlerHost

Which all have some message emission capabilities. This is too much. And having GlobalOutputHandler be a global instance adds more limitations on us.

Also - it would be really nice to add a lint rule that prevents direct terminal printouts (and ignore it where needed - like on ConsoleProgress).

}
}

export let globalOutputHandler = new GlobalOutputHandler();
Copy link
Contributor

Choose a reason for hiding this comment

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

Executing code on module load is not a great idea. Sure, right now all this code does is instantiate an empty object, but this can easily turn more heavy over time and either slow down or completely crash during import.

Instead, you can use the singleton pattern so that this code is only executed (once) whenever the first consumer needs it. That access pattern would be something like:

// for bits of the code that only read it
GlobalOutputHandler.get().verbose(...)

// for bits of the code that create and read it
GlobalOutputHandler.getOrCreate(listener)

@@ -113,6 +118,10 @@ export class AssetPublishing implements IPublishProgress {
},
dockerFactory: new DockerFactory(),
};
if (options.progressListener) {
globalOutputHandler.setListener(options.progressListener);
Copy link
Contributor

Choose a reason for hiding this comment

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

So what happens if the CDK (or any consumer) instantiates two instances of AssetPublishing with different listeners? If this is not expected behavior - protect against it. If this is expected behavior, how would it work?

Right now, this is a subtle bug waiting to happen.

this.totalOperations === 0
? 100
: Math.floor((this.completedOperations / this.totalOperations) * 100);
globalOutputHandler.setCompletionProgress(completionProgress);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same problem as with setting the listener, breaks on multiple instances of AssetPublishing


// AssetPublishing will assign the global output handler
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// AssetPublishing will assign the global output handler
// AssetPublishing will assign the progress listener to the global output handler

...options,
env: {
...process.env,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why?

options.logger(renderCommandLine(command));
}
globalOutputHandler.publishEvent(EventType.START, command.join(' '));
globalOutputHandler.info(renderCommandLine(command));
Copy link
Contributor

Choose a reason for hiding this comment

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

So from what I could gather, it used to be that this message would eventually be logged as:

logger: (m: string) => this.host.emitMessage(EventType.DEBUG, m),

But now it changed to:

cdk-assets/lib/progress.ts

Lines 125 to 127 in cacbcf4

public info(text: string) {
this.publishEvent(EventType.SUCCESS, text);
}

Is this what you meant to happen? Also, it used to be that this message was logged via the implenentation of IHostHandler:

emitMessage(t, m) {
self.progressEvent(t, m);
},

Which does some additional stuff apart from just logging. But now it just invokes the progress listener and it doesn't seem to be equivalent.

`${renderCommandLine(command)} exited with ${code != null ? 'error code' : 'signal'} ${code ?? signal}: ${out}`
)
const errorOutput = Buffer.concat(stderrChunks).toString('utf-8').trim();
const error_message = `${renderCommandLine(command)} exited with ${code != null ? 'error code' : 'signal'} ${code ?? signal}: ${errorOutput}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const error_message = `${renderCommandLine(command)} exited with ${code != null ? 'error code' : 'signal'} ${code ?? signal}: ${errorOutput}`;
const errorMessage = `${renderCommandLine(command)} exited with ${code != null ? 'error code' : 'signal'} ${code ?? signal}: ${errorOutput}`;

}

public info(text: string) {
this.publishEvent(EventType.SUCCESS, text);
Copy link
Contributor

@iliapolo iliapolo Dec 31, 2024

Choose a reason for hiding this comment

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

Mapping info to EventType.SUCCESS doesnt seem right. SUCCESS indicates that asset publishing has successfully finished, at least according to the doc string:

/**
* When an asset is successfully finished
*/
SUCCESS = 'success',

It might be the code currently doesn't make assumption on the event type being emitted, but thats likely to change and at some point calls to info will stop making sense. Also I see this is only used in shell.ts once, does it really merit a public method for it?

this.publishEvent(EventType.SUCCESS, text);
}

public hasListener() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not used. Should it be?

public publishEvent(eventType: EventType = EventType.DEBUG, text: string) {
const progressEvent: IPublishProgress = {
message: text,
abort: () => {},
Copy link
Contributor

Choose a reason for hiding this comment

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

This is bizarre. I think it might be functionally ok but signals a modeling problem IMO.

@HBobertz
Copy link
Contributor Author

HBobertz commented Dec 31, 2024

We already have the IPublishProgressListener interface for that, can we not just make sure this listener is being passed to whichever code that needs it? Yeah its a little bit more work but I think avoids some functional complications as mentioned in the comments. Introducing yet another "emitter" makes the code even more convoluted than it is now. We will have:

I originally started down this path but it ballooned into passing around the interface in a lot of places we really didn't need. However I hadn't considered the multiple assetPublishing instances. I think if only for that reason it makes sense to go down that path again. I'll pivot back to that

}
stdout.push(chunk);
stdoutChunks.push(chunk);
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably want to stream output to the logger. Docker builds can take 10 minutes, and it's not very helpful if you're sitting there, waiting, not knowing what's going on...

And then after 10 minutes you get a 2MB log dump of everything that happepend.

@@ -1,23 +1,23 @@
import * as child_process from 'child_process';
import { EventType, globalOutputHandler } from '../progress';
Copy link
Contributor

@rix0rrr rix0rrr Jan 9, 2025

Choose a reason for hiding this comment

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

Do not have this import here.

What was wrong with the Logger we had? That was a fully decoupled mechanism already. You might want to extend it with the notion of "types of message" (command line about to start, stdout chunk, stderr chunk, command finished), but you definitely don't want to use Asset Progress messages here just yet.

Pretend the following:

  • The shell helper already has the logging/notification features that you need, written by some other developer that knew exactly what you needed in advance.
  • Crucially: pretend it's a library we downloaded from NPM! It doesn't know anything about assets, and can't rely on anything else in our code. It cannot import any of the assets-specific files, and cannot use any of the types there.

Do this:

image

The "dependencies" strictly go to the right:

  • CLI knows about, and can depend on, asset publishing (but not the reverse!)
  • Asset publishing knows about, and can depend on, the shell helper (but not the reverse!)

If you need data to flow in the opposite direction, then you will have to use a callback mechanism with its own dedicated interface/type definitions.

As for this PR: I think I would actually recommend you start again from scratch with this in mind, and potentially copy/paste bits over from this PR if you decide you need them. Whatever you do, no global variables!

@HBobertz
Copy link
Contributor Author

HBobertz commented Jan 9, 2025

Close in favor of #295

@HBobertz HBobertz closed this Jan 9, 2025
auto-merge was automatically disabled January 9, 2025 22:24

Pull request was closed

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