-
Notifications
You must be signed in to change notification settings - Fork 2
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
Conversation
There was a problem hiding this 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.
BTW, if it doesn't close the issue, e.g. because a second PR is needed, you cannot put that in like that. |
Signed-off-by: github-actions <[email protected]>
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]>
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// AssetPublishing will assign the global output handler | |
// AssetPublishing will assign the progress listener to the global output handler |
...options, | ||
env: { | ||
...process.env, |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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:
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
:
Lines 116 to 118 in cacbcf4
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}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
There was a problem hiding this comment.
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:
Lines 22 to 25 in cacbcf4
/** | |
* 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() { |
There was a problem hiding this comment.
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: () => {}, |
There was a problem hiding this comment.
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.
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); |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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:
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!
Close in favor of #295 |
Pull request was closed
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 fromAssetPublishing
(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.