-
Notifications
You must be signed in to change notification settings - Fork 45
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
feat: add support for function logs streaming to sandbox #1492
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: b4b1506 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
const backendOutput: BackendOutput = | ||
await this.backendOutputClient.getOutput(sandboxBackendId); | ||
|
||
const definedFunctionsPayload = | ||
backendOutput[functionOutputKey]?.payload.definedFunctions; | ||
const deployedFunctionNames = definedFunctionsPayload | ||
? (JSON.parse(definedFunctionsPayload) as string[]) | ||
: []; |
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.
Can we use DeployedBackendClient.getBackendMetadata()
instead? That returns a list of FunctionConfiguration objects which should eliminate the need to depend on backend-output-schemas
directly in this package
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 shouldn't. DeployedBackendClient.getBackendMetadata()
is a pretty heavy command as it loads all the nested stacks and all resources.
We need something as lightweight as possible since this method gets called every time sandbox gets idle.
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.
Maybe we need some sort of "filter" prop for getBackendMetadata
where it can be instructed to only load metadata from certain verticals? So in this case, we could use it to only load function metadata
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 don't even need the function stack as technically what we are loading is not a metadata
unless we start adding the "friendly name" in the metadata or outputs section. In that case we can continue to use getOutput
here.
I also don't see any issues with depending on backend-output-schemas
here. backendOutputClient.getOutput
is the right abstraction here.
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.
Taking a closer look at this, it looks like we're only using the functionOutputKey from that package. However, getOutput
should already have typed keys so we can just use the string directly here and we'll get typechecking without having to import the schema package.
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 will update it to using the key "AWS::Amplify::Function"
directly, but wouldn't it just add duplicity. Not that we are ever going to change it but I don't understand the drawback of this dependency?
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.
It's not a huge deal. In my head, the deployed-backend-client
is the "entry point" by which all readers should get info about the backend. the schema
package is meant to be a contract between the backend-client
package and the places where the output is written (the constructs). Ideally the schema shouldn't be needed directly by consumers of deployed-backend-client
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.
It makes sense to have one writer, but I believe it's fine to have multiple readers. We already have client-config
and model-generator
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.
That's true but they both use deployed-backend-client
to get backend output. Neither of them goes directly to CFN
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 also coming from deployed-backed-client
const backendOutput: BackendOutput = await this.backendOutputClient.getOutput(sandboxBackendId);
Same as
amplify-backend/packages/cli/src/commands/generate/forms/generate_forms_command.ts
Line 69 in 20bf679
const output = await backendOutputClient.getOutput(backendIdentifier); |
backendOutputClient.getOutput(backendIdentifier) |
private readonly refreshRate: number = 500 | ||
) {} | ||
) { | ||
// if not running in tty, turn off colors |
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 is this class manipulating colors?
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.
Since this is a central place for performing logging, it's better to turn off/on colors here rather than at the consumers place. Especially since this $
is a global variable, the colors will only change when one consumer changes it making the colors inconsistent.
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.
Aren't the methods in format
already a noop if colors are not enabled? Why do we need additional logic for it here?
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.
format
uses supportColors
which only looks at the environment/terminal you are running in and decides whether to enable or disable colors. E.g. in CI/CD the colors might be disabled. See https://github.com/isaacs/color-support
In the printer, we might be piping the data in some other place as well (e.g. writing to a file) supportColors
doesn't know that and will keep the colors enabled.
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'm not sure this is right.
If $.enabled
is global setting but there could be two Printer
instances - one writing to console and the other to file - then how do we decide if colors should be enabled or not.
So either color control should not be in this class
OR color control should be implemented differently - for example given that node is single threaded try-finally blocks at each printX call could be used.
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 removed it from printer such that printer will print whatever is given to it. Instead of fiddling with kleur's $
the caller can then choose to not use format
or colors.
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.
Looks good overall.
.github/workflows/health_checks.yml
Outdated
- name: Install and build baseline version | ||
run: | | ||
npm ci | ||
npm run build |
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.
It took me a moment to recall what this was for.
Perhaps this could go into main in small dedicated PR?
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.
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'll remove this here once #1624 is merged, otherwise the e2e fails.
private readonly refreshRate: number = 500 | ||
) {} | ||
) { | ||
// if not running in tty, turn off colors |
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'm not sure this is right.
If $.enabled
is global setting but there could be two Printer
instances - one writing to console and the other to file - then how do we decide if colors should be enabled or not.
So either color control should not be in this class
OR color control should be implemented differently - for example given that node is single threaded try-finally blocks at each printX call could be used.
for (const logGroup of this.allLogGroups) { | ||
promises.push(this.readEventsFromLogGroup(logGroup)); | ||
} | ||
return (await Promise.all(promises)).flat(); |
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.
What happens if these calls start to fail, get throttled?
Would sandbox stop streaming or would it keep trying until problem goes away?
Should sandbox try for predefned period of time or number of times and give up at some point?
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.
Sandbox will keep trying with it's polling frequency, sandbox won't get interrupted through since log watching is running asynchronously.
Let me look into adding more error handling to perhaps stop streaming if too many unrecoverable errors happen.
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.
Fixed the error handling. We will now pause the streaming when an exception happens. Subsequent deployments if any will again resume the streaming if it has been fixed.
// As long as there are _any_ events in the log group `filterLogEvents` will return a nextToken. | ||
// This is true even if these events are before `startTime`. So if we have 100 events and a nextToken | ||
// then assume that we have hit the limit and let the user know some messages have been suppressed. | ||
// We are essentially showing them a sampling (10000 events printed out is not very useful) | ||
if (filteredEvents.length === 100 && response.nextToken) { |
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 that this shouldn't apply when we're streaming to file.
File is a different case and somebody may want to grep it/search it and missing entries will be surprising there.
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.
also, for console. should sampling be configurable through command parameters?
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.
also, for console. should sampling be configurable through command parameters?
This feature is exclusively for sandbox, so won't apply for console at all.
I think that this shouldn't apply when we're streaming to file.
There is a security aspect here as well to prevent overwhelming the machine with too many logs (DoS)
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 feature is exclusively for sandbox, so won't apply for console at all.
I meant console.log
vs file.write
scenario on local computer.
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.
Oh I see, that is not part of the requirements, but can be easily added in the future if needed.
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 this is going to be most wanted feature for people who stream logs to local file.
Mind creating GH issue?
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.
group: 'Logs streaming', | ||
}) | ||
.option('logs-filter', { | ||
describe: `Regex pattern to filter logs from only matched functions. E.g. to stream logs for a function, specify it's name, and to stream logs from all functions starting with auth specify 'auth' Default: Stream all logs`, |
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.
Shouldn't a regex for "functions starting with auth" be ^auth.*
? Also, why are we supporting regex and array input? You can make a regex to support multiple disjoint strings with (foo|bar|baz)
IMO if we are going to support regex, it should just be a single arg
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.
- For a full regex match yes, but I'm going for a partial string match as in my opinion it's easier to specify and also a default for pretty much all regex matchers. I don't see why we would prevent partial matching.
- Similarly providing array just makes it easier to provide input instead of creating complex regex pattern. @josefaidt to provide more input here.
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.
👋 yes the important piece of regex is the wildcard match (e.g. auth*
picks up all functions named with auth
like auth-post-confirmation
and auth-pre-signup
) but it is much easier to specify multiple patterns than it is to craft complex regex when writing the command
--logs-filter auth* --logs-filter resolver*
I think regex can be a bit of a hurdle though compared to a glob pattern
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 we want to support glob patterns or regex then? The current impl is using regex which would require --logs-filter auth.* logs-filter resolver.*
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.
glob patterns don't make sense for non pathlike strings. Globs are typically (or only?) used for pathname expansions.
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.
Yeah fair enough. Just want to make sure we're aligned that customers will have to specify auth.*
, not auth*
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.
There are use cases for glob patterns outside filenames
https://www.sqlitetutorial.net/sqlite-glob/
https://duckdb.org/docs/sql/functions/pattern_matching.html#glob
like minimatch without the globstar
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.
Minimatch is basically a glob implementation, still only relevant for pathlike string matching, not for any arbitrary matches.
In the SQL world, the glob
is basically implemented as a regex, e.g. there is no such thing as **
.
if (options.functionStreamingOptions?.logsOutFile) { | ||
this.functionsLogStreamer.setOutputLocation( | ||
options.functionStreamingOptions.logsOutFile | ||
); | ||
} |
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 seems like something that should be set in the ctor of the LambdaFunctionLogStreamer
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.
None of the sandbox options are provided to the SandboxSingletonFactory
like outDir
. I have changed it to pass this option in the start/activate like the BackendDeployer
instead of just a method for setOutputLocation
@@ -37,6 +37,13 @@ export type SandboxOptions = { | |||
format?: ClientConfigFormat; | |||
profile?: string; | |||
watchForChanges?: boolean; | |||
functionStreamingOptions?: SandboxFunctionStreamingOptions; |
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.
related to a comment above, I would remove this from the SandboxOptions and instead inject it directly into the LambdaFunctionLogStreamer
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.
Looks good, just left a few comments regarding what props get passed in to methods vs ctors
sandboxBackendId: BackendIdentifier, | ||
streamingOptions?: SandboxFunctionStreamingOptions |
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.
Since these parameters don't change over the lifecycle of this class, it seems like they should be part of the ctor
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.
See this #1492 (comment)
This is continuing the current DI pattern used everywhere. We are not passing the arguments to the factory for other commands as well. I'd like to keep this consistent and if needed have a refactor when required.
This is where we are instantiating these classes https://github.com/aws-amplify/amplify-backend/pull/1492/files#diff-2df4a084e13b31a8ac7a58aa826a88f39368636f33c58791294e267be36ac291R53
* If the file doesn't exist it will be created. | ||
* @param outputLocation file location | ||
*/ | ||
activate = (outputLocation?: string): void => { |
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.
Should outputLocation be part of the ctor?
Changes
Add support for streaming function logs in sandbox
CLI
--stream-function-logs
--once
should not stream function logsDisplay
Functionality
New permissions need to be added to the managed policy AmplifyBackendDeployFullAccess
Validation
Unit tests added
Checklist
run-e2e
label set.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.