Skip to content

Commit 8f23287

Browse files
authored
feat: add support for function logs streaming to sandbox (#1492)
* feat: add support for function logs streaming to sandbox * update package lock * update package lock * update package lock * PR feedback updates * PR feedback updates * PR feedback updates * try this * try this * try this * Updates to cli options * add more tests * fix lint * PR feedback updates * remove colors suppressions from printer * move ArnParser from cdk to sdk * fix error handling * PR updates * Remove color-support * regenerate package lock * update tests
1 parent 6295919 commit 8f23287

19 files changed

+9057
-6109
lines changed

.changeset/cool-coins-add.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@aws-amplify/cli-core': minor
3+
'@aws-amplify/sandbox': minor
4+
'@aws-amplify/backend-cli': minor
5+
'create-amplify': patch
6+
---
7+
8+
feat: add support for function logs streaming to sandbox

package-lock.json

Lines changed: 7622 additions & 6095 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli-core/API.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
/// <reference types="node" />
88

99
import { PackageManagerController } from '@aws-amplify/plugin-types';
10+
import { WriteStream } from 'node:tty';
1011

1112
// @public
1213
export class AmplifyPrompter {
@@ -21,12 +22,20 @@ export class AmplifyPrompter {
2122
}) => Promise<boolean>;
2223
}
2324

25+
// @public (undocumented)
26+
export type ColorName = (typeof colorNames)[number];
27+
28+
// @public (undocumented)
29+
export const colorNames: readonly ["Green", "Yellow", "Blue", "Magenta", "Cyan"];
30+
2431
// @public
2532
export class Format {
2633
constructor(packageManagerRunnerName?: string);
2734
// (undocumented)
2835
bold: (message: string) => string;
2936
// (undocumented)
37+
color: (message: string, colorName: ColorName) => string;
38+
// (undocumented)
3039
command: (command: string) => string;
3140
// (undocumented)
3241
dim: (message: string) => string;
@@ -73,7 +82,7 @@ export class PackageManagerControllerFactory {
7382

7483
// @public
7584
export class Printer {
76-
constructor(minimumLogLevel: LogLevel, stdout?: NodeJS.WriteStream, stderr?: NodeJS.WriteStream, refreshRate?: number);
85+
constructor(minimumLogLevel: LogLevel, stdout?: WriteStream | NodeJS.WritableStream, stderr?: WriteStream | NodeJS.WritableStream, refreshRate?: number);
7786
indicateProgress(message: string, callback: () => Promise<void>): Promise<void>;
7887
log(message: string, level?: LogLevel): void;
7988
print: (message: string) => void;

packages/cli-core/src/format/format.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import * as os from 'node:os';
22
import * as assert from 'node:assert';
3-
import { after, before, describe, it } from 'node:test';
3+
import { after, before, beforeEach, describe, it } from 'node:test';
44
import { Format, format } from './format.js';
55
import { $, blue, bold, cyan, green, red, underline } from 'kleur/colors';
66

77
void describe('format', () => {
8+
beforeEach(() => {
9+
$.enabled = true;
10+
});
11+
812
void it('should format ampx command with yarn', { concurrency: 1 }, () => {
913
const formatter = new Format('yarn');
1014
assert.strictEqual(
@@ -157,3 +161,13 @@ void describe('format when terminal colors disabled', async () => {
157161
assert.strictEqual(coloredMessage, 'npx ampx hello');
158162
});
159163
});
164+
165+
void describe('format.color', async () => {
166+
void it('should format colors as requested', () => {
167+
const input = 'something to color';
168+
const expectedOutput = green(input);
169+
const actualOutput = format.color(input, 'Green');
170+
assert.strictEqual(actualOutput, expectedOutput);
171+
assert.notStrictEqual(actualOutput, red(input)); // confirm that coloring actually works
172+
});
173+
});

packages/cli-core/src/format/format.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import * as os from 'node:os';
22
import {
3+
Colorize,
34
blue,
45
bold,
56
cyan,
67
dim,
78
green,
89
grey,
10+
magenta,
911
red,
1012
underline,
13+
yellow,
1114
} from 'kleur/colors';
1215
import { AmplifyFault } from '@aws-amplify/platform-core';
1316
import { getPackageManagerRunnerName } from '../package-manager-controller/get_package_manager_name.js';
@@ -71,6 +74,26 @@ export class Format {
7174
Object.entries(record)
7275
.map(([key, value]) => `${key}: ${String(value)}`)
7376
.join(os.EOL);
77+
color = (message: string, colorName: ColorName) => colors[colorName](message);
7478
}
7579

80+
// Map to connect colorName to kleur color
81+
const colors: Record<ColorName, Colorize> = {
82+
Green: green,
83+
Yellow: yellow,
84+
Blue: blue,
85+
Magenta: magenta,
86+
Cyan: cyan,
87+
};
88+
89+
export const colorNames = [
90+
'Green',
91+
'Yellow',
92+
'Blue',
93+
'Magenta',
94+
'Cyan',
95+
] as const;
96+
97+
export type ColorName = (typeof colorNames)[number];
98+
7699
export const format = new Format();

packages/cli-core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export * from './prompter/amplify_prompts.js';
22
export * from './printer/printer.js';
33
export * from './printer.js';
4-
export { format, Format } from './format/format.js';
4+
export { ColorName, colorNames, format, Format } from './format/format.js';
55
export * from './package-manager-controller/package_manager_controller_factory.js';

packages/cli-core/src/printer/printer.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { WriteStream } from 'node:tty';
12
import { EOL } from 'os';
2-
33
export type RecordValue = string | number | string[] | Date;
44

55
/**
@@ -19,8 +19,12 @@ export class Printer {
1919
*/
2020
constructor(
2121
private readonly minimumLogLevel: LogLevel,
22-
private readonly stdout: NodeJS.WriteStream = process.stdout,
23-
private readonly stderr: NodeJS.WriteStream = process.stderr,
22+
private readonly stdout:
23+
| WriteStream
24+
| NodeJS.WritableStream = process.stdout,
25+
private readonly stderr:
26+
| WriteStream
27+
| NodeJS.WritableStream = process.stderr,
2428
private readonly refreshRate: number = 500
2529
) {}
2630

@@ -91,7 +95,7 @@ export class Printer {
9195
* Checks if the environment is TTY
9296
*/
9397
private isTTY() {
94-
return this.stdout.isTTY;
98+
return this.stdout instanceof WriteStream && this.stdout.isTTY;
9599
}
96100

97101
/**

packages/cli/src/commands/sandbox/sandbox_command.test.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import { AmplifyPrompter, format, printer } from '@aws-amplify/cli-core';
1212
import { EventHandler, SandboxCommand } from './sandbox_command.js';
1313
import { createSandboxCommand } from './sandbox_command_factory.js';
1414
import { SandboxDeleteCommand } from './sandbox-delete/sandbox_delete_command.js';
15-
import { Sandbox, SandboxSingletonFactory } from '@aws-amplify/sandbox';
15+
import {
16+
Sandbox,
17+
SandboxFunctionStreamingOptions,
18+
SandboxSingletonFactory,
19+
} from '@aws-amplify/sandbox';
1620
import { createSandboxSecretCommand } from './sandbox-secret/sandbox_secret_command_factory.js';
1721
import { ClientConfigGeneratorAdapter } from '../../client-config/client_config_generator_adapter.js';
1822
import { CommandMiddleware } from '../../command_middleware.js';
@@ -118,6 +122,9 @@ void describe('sandbox command', () => {
118122
assert.match(output, /--outputs-format/);
119123
assert.match(output, /--outputs-out-dir/);
120124
assert.match(output, /--once/);
125+
assert.match(output, /--stream-function-logs/);
126+
assert.match(output, /--logs-filter/);
127+
assert.match(output, /--logs-out-file/);
121128
assert.equal(mockHandleProfile.mock.callCount(), 0);
122129
});
123130

@@ -341,7 +348,7 @@ void describe('sandbox command', () => {
341348
);
342349
});
343350

344-
void it('--once flag is mutually exclusive with dir-to-watch & exclude', async () => {
351+
void it('--once flag is mutually exclusive with dir-to-watch, exclude and stream-function-logs', async () => {
345352
assert.match(
346353
await commandRunner.runCommand(
347354
'sandbox --once --dir-to-watch nonExistentDir'
@@ -352,5 +359,31 @@ void describe('sandbox command', () => {
352359
await commandRunner.runCommand('sandbox --once --exclude test'),
353360
/Arguments once and exclude are mutually exclusive/
354361
);
362+
assert.match(
363+
await commandRunner.runCommand('sandbox --once --stream-function-logs'),
364+
/Arguments once and stream-function-logs are mutually exclusive/
365+
);
366+
});
367+
368+
void it('fails if --logs-out-file is provided without enabling --stream-function-logs', async () => {
369+
assert.match(
370+
await commandRunner.runCommand('sandbox --logs-out-file someFile'),
371+
/Missing dependent arguments:\n logs-out-file -> stream-function-logs/
372+
);
373+
});
374+
375+
void it('starts sandbox with log watching options', async () => {
376+
await commandRunner.runCommand(
377+
'sandbox --stream-function-logs --logs-filter func1 --logs-filter func2 --logs-out-file someFile'
378+
);
379+
assert.equal(sandboxStartMock.mock.callCount(), 1);
380+
assert.deepStrictEqual(
381+
sandboxStartMock.mock.calls[0].arguments[0].functionStreamingOptions,
382+
{
383+
enabled: true,
384+
logsFilters: ['func1', 'func2'],
385+
logsOutFile: 'someFile',
386+
} as SandboxFunctionStreamingOptions
387+
);
355388
});
356389
});

packages/cli/src/commands/sandbox/sandbox_command.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs';
22
import fs from 'fs';
33
import fsp from 'fs/promises';
44
import { AmplifyPrompter } from '@aws-amplify/cli-core';
5-
import { SandboxSingletonFactory } from '@aws-amplify/sandbox';
5+
import {
6+
SandboxFunctionStreamingOptions,
7+
SandboxSingletonFactory,
8+
} from '@aws-amplify/sandbox';
69
import {
710
ClientConfigFormat,
811
ClientConfigVersion,
@@ -26,6 +29,9 @@ export type SandboxCommandOptionsKebabCase = ArgumentsKebabCase<
2629
outputsOutDir: string | undefined;
2730
outputsVersion: string;
2831
once: boolean | undefined;
32+
streamFunctionLogs: boolean | undefined;
33+
logsFilter: string[] | undefined;
34+
logsOutFile: string | undefined;
2935
} & SandboxCommandGlobalOptions
3036
>;
3137

@@ -123,12 +129,29 @@ export class SandboxCommand
123129
}
124130

125131
watchExclusions.push(clientConfigWritePath);
132+
133+
let functionStreamingOptions: SandboxFunctionStreamingOptions = {
134+
enabled: false,
135+
};
136+
if (args.streamFunctionLogs) {
137+
// turn on function logs streaming
138+
functionStreamingOptions = {
139+
enabled: true,
140+
logsFilters: args.logsFilter,
141+
logsOutFile: args.logsOutFile,
142+
};
143+
144+
if (args.logsOutFile) {
145+
watchExclusions.push(args.logsOutFile);
146+
}
147+
}
126148
await sandbox.start({
127149
dir: args.dirToWatch,
128150
exclude: watchExclusions,
129151
identifier: args.identifier,
130152
profile: args.profile,
131153
watchForChanges: !args.once,
154+
functionStreamingOptions,
132155
});
133156
process.once('SIGINT', () => void this.sigIntHandler());
134157
};
@@ -196,6 +219,30 @@ export class SandboxCommand
196219
boolean: true,
197220
global: false,
198221
})
222+
.option('stream-function-logs', {
223+
describe:
224+
'Whether to stream function execution logs. Default: false. Use --logs-filter in addition to this flag to stream specific function logs',
225+
boolean: true,
226+
global: false,
227+
group: 'Logs streaming',
228+
})
229+
.option('logs-filter', {
230+
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`,
231+
array: true,
232+
type: 'string',
233+
group: 'Logs streaming',
234+
implies: ['stream-function-logs'],
235+
requiresArg: true,
236+
})
237+
.option('logs-out-file', {
238+
describe:
239+
'File to append the streaming logs. The file is created if it does not exist. Default: stdout',
240+
array: false,
241+
type: 'string',
242+
group: 'Logs streaming',
243+
implies: ['stream-function-logs'],
244+
requiresArg: true,
245+
})
199246
.check(async (argv) => {
200247
if (argv['dir-to-watch']) {
201248
await this.validateDirectory('dir-to-watch', argv['dir-to-watch']);
@@ -210,7 +257,13 @@ export class SandboxCommand
210257
}
211258
return true;
212259
})
213-
.conflicts('once', ['exclude', 'dir-to-watch'])
260+
.conflicts('once', [
261+
'exclude',
262+
'dir-to-watch',
263+
'stream-function-logs',
264+
'logs-filter',
265+
'logs-out-file',
266+
])
214267
.middleware([this.commandMiddleware.ensureAwsCredentialAndRegion])
215268
);
216269
};

packages/sandbox/API.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ export type SandboxDeleteOptions = {
3030
// @public (undocumented)
3131
export type SandboxEvents = 'successfulDeployment' | 'failedDeployment' | 'successfulDeletion';
3232

33+
// @public (undocumented)
34+
export type SandboxFunctionStreamingOptions = {
35+
enabled: boolean;
36+
logsFilters?: string[];
37+
logsOutFile?: string;
38+
};
39+
3340
// @public (undocumented)
3441
export type SandboxOptions = {
3542
dir?: string;
@@ -38,6 +45,7 @@ export type SandboxOptions = {
3845
format?: ClientConfigFormat;
3946
profile?: string;
4047
watchForChanges?: boolean;
48+
functionStreamingOptions?: SandboxFunctionStreamingOptions;
4149
};
4250

4351
// @public

0 commit comments

Comments
 (0)