Skip to content

Commit 7000b80

Browse files
jesseturner21claude
andcommitted
feat: enable CloudWatch Transaction Search on deploy
After a successful deploy with agents, the CLI now checks if CloudWatch Application Signals (which powers Transaction Search) is enabled and auto-enables it via StartDiscovery when --yes is passed. A console URL is added to the next-steps output so users can view traces immediately. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a158ffb commit 7000b80

9 files changed

Lines changed: 719 additions & 392 deletions

File tree

package-lock.json

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

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,15 @@
6868
},
6969
"dependencies": {
7070
"@aws-cdk/toolkit-lib": "^1.16.0",
71+
"@aws-sdk/client-application-signals": "^3.1003.0",
7172
"@aws-sdk/client-bedrock-agentcore": "^3.893.0",
7273
"@aws-sdk/client-bedrock-agentcore-control": "^3.893.0",
7374
"@aws-sdk/client-bedrock-runtime": "^3.893.0",
7475
"@aws-sdk/client-cloudformation": "^3.893.0",
7576
"@aws-sdk/client-cloudwatch-logs": "^3.893.0",
7677
"@aws-sdk/client-resource-groups-tagging-api": "^3.893.0",
7778
"@aws-sdk/client-sts": "^3.893.0",
79+
"@aws-sdk/client-xray": "^3.1003.0",
7880
"@aws-sdk/credential-providers": "^3.893.0",
7981
"@commander-js/extra-typings": "^14.0.0",
8082
"@smithy/shared-ini-file-loader": "^4.4.2",

src/cli/aws/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ export {
1414
type GetAgentRuntimeStatusOptions,
1515
} from './agentcore-control';
1616
export { streamLogs, searchLogs, type LogEvent, type StreamLogsOptions, type SearchLogsOptions } from './cloudwatch';
17+
export {
18+
checkTransactionSearchEnabled,
19+
enableTransactionSearch,
20+
buildTransactionSearchConsoleUrl,
21+
type TransactionSearchStatus,
22+
type TransactionSearchEnableResult,
23+
} from './transaction-search';
1724
export {
1825
DEFAULT_RUNTIME_USER_ID,
1926
invokeAgentRuntime,

src/cli/aws/transaction-search.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { getCredentialProvider } from './account';
2+
import {
3+
ApplicationSignalsClient,
4+
StartDiscoveryCommand,
5+
} from '@aws-sdk/client-application-signals';
6+
import { GetGroupsCommand, XRayClient } from '@aws-sdk/client-xray';
7+
8+
export interface TransactionSearchStatus {
9+
enabled: boolean;
10+
error?: string;
11+
}
12+
13+
export interface TransactionSearchEnableResult {
14+
success: boolean;
15+
error?: string;
16+
}
17+
18+
/**
19+
* Check if CloudWatch Application Signals (which powers transaction search) is enabled
20+
* by attempting to list X-Ray groups. If X-Ray is accessible, the account has tracing active.
21+
* We also try StartDiscovery as an idempotent check — if it succeeds, it was either already
22+
* enabled or is now enabled.
23+
*/
24+
export async function checkTransactionSearchEnabled(region: string): Promise<TransactionSearchStatus> {
25+
try {
26+
const xrayClient = new XRayClient({
27+
region,
28+
credentials: getCredentialProvider(),
29+
});
30+
await xrayClient.send(new GetGroupsCommand({}));
31+
return { enabled: true };
32+
} catch (err: unknown) {
33+
const code = (err as { name?: string })?.name;
34+
if (code === 'AccessDeniedException' || code === 'AccessDenied') {
35+
return { enabled: false, error: 'Insufficient permissions to check X-Ray status' };
36+
}
37+
// If the call fails for other reasons, assume not enabled / unknown
38+
return { enabled: false };
39+
}
40+
}
41+
42+
/**
43+
* Enable CloudWatch Application Signals by calling StartDiscovery.
44+
* This creates the AWSServiceRoleForCloudWatchApplicationSignals service-linked role
45+
* and enables transaction search in the CloudWatch console.
46+
*
47+
* This is an idempotent operation — calling it when already enabled is a no-op.
48+
*/
49+
export async function enableTransactionSearch(region: string): Promise<TransactionSearchEnableResult> {
50+
try {
51+
const client = new ApplicationSignalsClient({
52+
region,
53+
credentials: getCredentialProvider(),
54+
});
55+
await client.send(new StartDiscoveryCommand({}));
56+
return { success: true };
57+
} catch (err: unknown) {
58+
const code = (err as { name?: string })?.name;
59+
const message = (err as { message?: string })?.message ?? 'Unknown error';
60+
61+
if (code === 'AccessDeniedException' || code === 'AccessDenied') {
62+
return {
63+
success: false,
64+
error: `Insufficient IAM permissions to enable Application Signals. Required: application-signals:StartDiscovery. ${message}`,
65+
};
66+
}
67+
return { success: false, error: `Failed to enable Application Signals: ${message}` };
68+
}
69+
}
70+
71+
/**
72+
* Build a deep-link URL to the CloudWatch Transaction Search console page.
73+
*/
74+
export function buildTransactionSearchConsoleUrl(region: string): string {
75+
return `https://${region}.console.aws.amazon.com/cloudwatch/home?region=${region}#xray:traces/query`;
76+
}

src/cli/commands/deploy/actions.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
performStackTeardown,
2323
setupApiKeyProviders,
2424
setupOAuth2Providers,
25+
setupTransactionSearch,
2526
synthesizeCdk,
2627
validateProject,
2728
} from '../../operations/deploy';
@@ -415,6 +416,32 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
415416

416417
endStep('success');
417418

419+
// Post-deploy: Enable CloudWatch Transaction Search (non-blocking)
420+
const nextSteps = agentNames.length > 0 ? [...AGENT_NEXT_STEPS] : [...MEMORY_ONLY_NEXT_STEPS];
421+
if (agentNames.length > 0) {
422+
try {
423+
startStep('Enable transaction search');
424+
const tsResult = await setupTransactionSearch({
425+
region: target.region,
426+
agentNames,
427+
autoConfirm: options.autoConfirm,
428+
});
429+
if (tsResult.error) {
430+
logger.log(`Transaction search setup warning: ${tsResult.error}`, 'warn');
431+
}
432+
if (tsResult.consoleUrl) {
433+
nextSteps.push(`View traces: ${tsResult.consoleUrl}`);
434+
}
435+
if (tsResult.skipped) {
436+
logger.log('Transaction search setup skipped (use --yes to auto-enable)', 'info');
437+
}
438+
endStep('success');
439+
} catch (err: unknown) {
440+
logger.log(`Transaction search setup failed: ${getErrorMessage(err)}`, 'warn');
441+
endStep('success'); // Non-blocking: don't mark as error
442+
}
443+
}
444+
418445
logger.finalize(true);
419446

420447
return {
@@ -423,7 +450,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
423450
stackName,
424451
outputs,
425452
logPath: logger.getRelativeLogPath(),
426-
nextSteps: agentNames.length > 0 ? AGENT_NEXT_STEPS : MEMORY_ONLY_NEXT_STEPS,
453+
nextSteps,
427454
};
428455
} catch (err: unknown) {
429456
logger.log(getErrorMessage(err), 'error');

src/cli/operations/deploy/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ export {
4242
type StackTeardownResult,
4343
} from './teardown';
4444

45+
// Post-deploy observability setup
46+
export {
47+
setupTransactionSearch,
48+
type TransactionSearchSetupOptions,
49+
type TransactionSearchSetupResult,
50+
} from './post-deploy-observability';
51+
4552
// Re-export external requirements for convenience
4653
export {
4754
checkDependencyVersions,
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {
2+
buildTransactionSearchConsoleUrl,
3+
checkTransactionSearchEnabled,
4+
enableTransactionSearch,
5+
} from '../../aws/transaction-search';
6+
7+
export interface TransactionSearchSetupOptions {
8+
region: string;
9+
agentNames: string[];
10+
autoConfirm?: boolean;
11+
}
12+
13+
export interface TransactionSearchSetupResult {
14+
success: boolean;
15+
enabled: boolean;
16+
skipped: boolean;
17+
consoleUrl?: string;
18+
error?: string;
19+
}
20+
21+
/**
22+
* Post-deploy step: check if CloudWatch Transaction Search is enabled, and enable it if not.
23+
* This is a non-blocking best-effort operation — failures do not fail the deploy.
24+
*/
25+
export async function setupTransactionSearch(
26+
options: TransactionSearchSetupOptions
27+
): Promise<TransactionSearchSetupResult> {
28+
const { region, agentNames, autoConfirm } = options;
29+
30+
if (agentNames.length === 0) {
31+
return { success: true, enabled: false, skipped: true };
32+
}
33+
34+
// Check current status
35+
const status = await checkTransactionSearchEnabled(region);
36+
37+
if (status.enabled) {
38+
return {
39+
success: true,
40+
enabled: true,
41+
skipped: false,
42+
consoleUrl: buildTransactionSearchConsoleUrl(region),
43+
};
44+
}
45+
46+
// Not enabled — attempt to enable if autoConfirm is set
47+
// In the CLI (non-TUI) path, autoConfirm corresponds to --yes flag
48+
// In the TUI path, we always attempt since the user has already confirmed deploy
49+
if (!autoConfirm) {
50+
return { success: true, enabled: false, skipped: true };
51+
}
52+
53+
const enableResult = await enableTransactionSearch(region);
54+
55+
if (!enableResult.success) {
56+
return {
57+
success: false,
58+
enabled: false,
59+
skipped: false,
60+
error: enableResult.error,
61+
};
62+
}
63+
64+
return {
65+
success: true,
66+
enabled: true,
67+
skipped: false,
68+
consoleUrl: buildTransactionSearchConsoleUrl(region),
69+
};
70+
}

src/cli/tui/screens/deploy/useDeployFlow.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from '../../../cloudformation';
1010
import { getErrorMessage, isChangesetInProgressError, isExpiredTokenError } from '../../../errors';
1111
import { ExecLogger } from '../../../logging';
12-
import { performStackTeardown } from '../../../operations/deploy';
12+
import { performStackTeardown, setupTransactionSearch } from '../../../operations/deploy';
1313
import { getGatewayTargetStatuses } from '../../../operations/deploy/gateway-status';
1414
import {
1515
type StackDiffSummary,
@@ -372,6 +372,28 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
372372
const message = error instanceof Error ? error.message : 'Unknown error';
373373
logger.log(`Failed to persist deployed state: ${message}`, 'warn');
374374
}
375+
376+
// Post-deploy: Enable CloudWatch Transaction Search (non-blocking)
377+
const agentNames = context?.projectSpec.agents?.map((a: { name: string }) => a.name) ?? [];
378+
const targetRegion = context?.awsTargets[0]?.region;
379+
if (agentNames.length > 0 && targetRegion) {
380+
try {
381+
const tsResult = await setupTransactionSearch({
382+
region: targetRegion,
383+
agentNames,
384+
autoConfirm: true,
385+
});
386+
if (tsResult.error) {
387+
logger.log(`Transaction search setup warning: ${tsResult.error}`, 'warn');
388+
}
389+
if (tsResult.consoleUrl) {
390+
logger.log(`Transaction search enabled: ${tsResult.consoleUrl}`);
391+
}
392+
} catch (error) {
393+
const message = error instanceof Error ? error.message : 'Unknown error';
394+
logger.log(`Transaction search setup failed: ${message}`, 'warn');
395+
}
396+
}
375397
}
376398

377399
logger.endStep('success');
@@ -433,6 +455,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
433455
switchableIoHost,
434456
context?.isTeardownDeploy,
435457
context?.awsTargets,
458+
context?.projectSpec.agents,
436459
diffMode,
437460
]);
438461

src/schema/schemas/deployed-state.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,16 @@ export const CredentialDeployedStateSchema = z.object({
119119

120120
export type CredentialDeployedState = z.infer<typeof CredentialDeployedStateSchema>;
121121

122+
// ============================================================================
123+
// Observability State
124+
// ============================================================================
125+
126+
export const ObservabilityStateSchema = z.object({
127+
transactionSearchEnabled: z.boolean(),
128+
});
129+
130+
export type ObservabilityState = z.infer<typeof ObservabilityStateSchema>;
131+
122132
// ============================================================================
123133
// Deployed Resource State
124134
// ============================================================================
@@ -131,6 +141,7 @@ export const DeployedResourceStateSchema = z.object({
131141
credentials: z.record(z.string(), CredentialDeployedStateSchema).optional(),
132142
stackName: z.string().optional(),
133143
identityKmsKeyArn: z.string().optional(),
144+
observability: ObservabilityStateSchema.optional(),
134145
});
135146

136147
export type DeployedResourceState = z.infer<typeof DeployedResourceStateSchema>;

0 commit comments

Comments
 (0)