Skip to content

Commit b593481

Browse files
authored
Merge pull request #301 from Azure/copilot/improve-extract-and-publish-log-readability
Improve extract and publish log readability: tier grouping, timing, and per-resource retry reporting
2 parents d23cb40 + f2326a5 commit b593481

14 files changed

Lines changed: 954 additions & 44 deletions

‎src/cli/extract-command.ts‎

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import { ArtifactStore } from '../clients/artifact-store.js';
2121
import { IArtifactStore } from '../clients/iartifact-store.js';
2222
import { getCloudConfig, buildArmBaseUrl } from '../lib/cloud-config.js';
2323
import { EXIT_FATAL, EXIT_SUCCESS } from '../lib/exit-codes.js';
24+
import { getResourceTier, TIER_LABELS } from '../lib/dependency-graph.js';
25+
import { formatDuration } from '../lib/format-duration.js';
26+
import { ResourceType } from '../models/resource-types.js';
27+
import { getNamePart } from '../lib/resource-path.js';
2428

2529
/**
2630
* Interface for extract command options (from CLI flags).
@@ -135,6 +139,7 @@ export async function executeExtract(
135139
await fs.mkdir(outputParent, { recursive: true });
136140
const stagingDir = await fs.mkdtemp(stagingPrefix);
137141

142+
const startedAt = Date.now();
138143
let result: ExtractionResult;
139144
try {
140145
const extractConfig: ExtractConfig = {
@@ -159,11 +164,13 @@ export async function executeExtract(
159164
await fs.rm(stagingDir, { recursive: true, force: true });
160165
}
161166

167+
const elapsedMs = Date.now() - startedAt;
168+
162169
// Output results
163170
if (globalOpts.format === 'json') {
164-
outputJson(result);
171+
outputJson(result, elapsedMs);
165172
} else {
166-
outputText(result);
173+
outputText(result, elapsedMs);
167174
}
168175

169176
dependencies.exit(result.exitCode);
@@ -180,13 +187,14 @@ export function shouldRemoveStaleArtifacts(
180187
* JSON output mode for extract.
181188
* Machine-readable JSON to stdout with resource counts and file paths.
182189
*/
183-
function outputJson(result: ExtractionResult): void {
190+
function outputJson(result: ExtractionResult, elapsedMs: number): void {
184191
const output = {
185192
status: result.exitCode === 0 ? 'success' : result.exitCode === 1 ? 'partial' : 'error',
186193
exitCode: result.exitCode,
187194
summary: {
188195
totalExtracted: result.totalExtracted,
189196
totalErrors: result.totalErrors,
197+
elapsedMs,
190198
typeBreakdown: result.typeResults.map((tr) => ({
191199
type: tr.type,
192200
extracted: tr.extracted.filter((r) => r.status === 'success').length,
@@ -216,30 +224,72 @@ function outputJson(result: ExtractionResult): void {
216224
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
217225
}
218226

227+
228+
function tierOf(type: ResourceType): number {
229+
try {
230+
return getResourceTier(type);
231+
} catch {
232+
return Number.MAX_SAFE_INTEGER;
233+
}
234+
}
235+
219236
/**
220237
* Text output mode (default) — per-resource status lines.
221238
*/
222-
function outputText(result: ExtractionResult): void {
223-
// Per-type summary
239+
export function outputText(result: ExtractionResult, elapsedMs: number): void {
240+
// Per-type summary, grouped by dependency tier
241+
const linesByTier = new Map<number, string[]>();
224242
for (const tr of result.typeResults) {
243+
const lines: string[] = [];
225244
const successCount = tr.extracted.filter((r) => r.status === 'success').length;
226245
if (successCount > 0) {
227-
process.stdout.write(`Extracted ${successCount} ${tr.type}(s)\n`);
246+
lines.push(` Extracted ${successCount} ${tr.type}(s)`);
228247
}
229248
if (tr.errorCount > 0) {
230-
process.stdout.write(`Failed ${tr.errorCount} ${tr.type}(s)\n`);
249+
lines.push(` Failed ${tr.errorCount} ${tr.type}(s)`);
250+
}
251+
if (lines.length === 0) continue;
252+
253+
const tier = tierOf(tr.type);
254+
const tierLines = linesByTier.get(tier) ?? [];
255+
tierLines.push(...lines);
256+
linesByTier.set(tier, tierLines);
257+
}
258+
259+
for (const tier of [...linesByTier.keys()].sort((a, b) => a - b)) {
260+
const label = TIER_LABELS[tier];
261+
process.stdout.write(label ? `Tier ${tier}: ${label}\n` : 'Other resources\n');
262+
process.stdout.write(`${linesByTier.get(tier)!.join('\n')}\n\n`);
263+
}
264+
265+
// API details — list every extracted API, even those without sub-resources
266+
// or whose sub-resource extraction failed
267+
const apiDetails = new Map(result.apiResults.map((ar) => [ar.apiName, ar]));
268+
const apiNames = new Set<string>();
269+
for (const tr of result.typeResults) {
270+
if (tr.type !== ResourceType.Api) continue;
271+
for (const r of tr.extracted) {
272+
if (r.status === 'success') apiNames.add(getNamePart(r.descriptor.nameParts, 0));
231273
}
232274
}
275+
for (const name of apiDetails.keys()) apiNames.add(name);
233276

234-
// API details
235-
for (const ar of result.apiResults) {
236-
const details: string[] = [];
237-
if (ar.specification) details.push('spec');
238-
if (ar.operations.length > 0) details.push(`${ar.operations.length} ops`);
239-
if (ar.revisions.length > 0) details.push(`${ar.revisions.length} revisions`);
240-
if (details.length > 0) {
241-
process.stdout.write(` API "${ar.apiName}": ${details.join(', ')}\n`);
277+
if (apiNames.size > 0) {
278+
process.stdout.write('APIs:\n');
279+
}
280+
for (const name of apiNames) {
281+
const ar = apiDetails.get(name);
282+
let summary: string;
283+
if (!ar) {
284+
summary = 'sub-resource extraction failed';
285+
} else {
286+
const details: string[] = [];
287+
if (ar.specification) details.push('spec');
288+
if (ar.operations.length > 0) details.push(`${ar.operations.length} ops`);
289+
if (ar.revisions.length > 0) details.push(`${ar.revisions.length} revisions`);
290+
summary = details.length > 0 ? details.join(', ') : 'definition only';
242291
}
292+
process.stdout.write(` API "${name}": ${summary}\n`);
243293
}
244294

245295
// Workspace details
@@ -249,6 +299,7 @@ function outputText(result: ExtractionResult): void {
249299

250300
// Summary
251301
process.stdout.write(
252-
`\nTotal: ${result.totalExtracted} resources extracted, ${result.totalErrors} errors\n`
302+
`\nTotal: ${result.totalExtracted} resources extracted, ${result.totalErrors} errors ` +
303+
`in ${formatDuration(elapsedMs)}\n`
253304
);
254305
}

‎src/cli/publish-command.ts‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { logger, parseLogLevel } from '../lib/logger.js';
1616
import { ApimClient } from '../clients/apim-client.js';
1717
import { ArtifactStore } from '../clients/artifact-store.js';
1818
import { getCloudConfig, buildArmBaseUrl } from '../lib/cloud-config.js';
19+
import { formatDuration } from '../lib/format-duration.js';
1920

2021
/**
2122
* Interface for publish command options (from CLI flags).
@@ -158,6 +159,7 @@ async function executePublish(
158159
deleteUnmatched: options.deleteUnmatched,
159160
commitId,
160161
logLevel: parseLogLevel(globalOpts.logLevel ?? 'info'),
162+
outputFormat: globalOpts.format === 'json' ? 'json' : 'text',
161163
};
162164

163165
// Create client and store
@@ -202,13 +204,17 @@ function outputJson(result: PublishResult): void {
202204
totalDeletes: number;
203205
totalErrors: number;
204206
totalSkipped: number;
207+
totalRetries: number;
208+
retriedResources: number;
209+
elapsedMs?: number;
205210
};
206211
actions: Array<{
207212
action: string;
208213
type: string;
209214
nameParts: string[];
210215
status: string;
211216
error?: string;
217+
retries?: number;
212218
}>;
213219
dryRun?: {
214220
actions: Array<{
@@ -238,13 +244,17 @@ function outputJson(result: PublishResult): void {
238244
totalDeletes: result.totalDeletes,
239245
totalErrors: result.totalErrors,
240246
totalSkipped: result.totalSkipped,
247+
totalRetries: result.totalRetries ?? 0,
248+
retriedResources: result.retriedResources ?? 0,
249+
elapsedMs: result.elapsedMs,
241250
},
242251
actions: result.actions.map((action) => ({
243252
action: action.action,
244253
type: action.descriptor.type,
245254
nameParts: action.descriptor.nameParts,
246255
status: action.status,
247256
error: action.error?.message,
257+
retries: action.retries,
248258
})),
249259
};
250260

@@ -268,7 +278,7 @@ function outputJson(result: PublishResult): void {
268278
/**
269279
* Text output mode (default) — per-resource status lines and summary.
270280
*/
271-
function outputText(result: PublishResult, dryRun: boolean): void {
281+
export function outputText(result: PublishResult, dryRun: boolean): void {
272282
// Per-resource status lines are already output by publish-service
273283
// Just output the summary here
274284

@@ -300,5 +310,18 @@ function outputText(result: PublishResult, dryRun: boolean): void {
300310
if (result.totalErrors > 0) {
301311
process.stdout.write(`${result.totalErrors} errors\n`);
302312
}
313+
314+
const totalRetries = result.totalRetries ?? 0;
315+
if (totalRetries > 0) {
316+
const retriedResources = result.retriedResources ?? 0;
317+
process.stdout.write(
318+
`${totalRetries} ${totalRetries === 1 ? 'retry' : 'retries'} across ` +
319+
`${retriedResources} ${retriedResources === 1 ? 'resource' : 'resources'}\n`
320+
);
321+
}
322+
}
323+
324+
if (result.elapsedMs !== undefined) {
325+
process.stdout.write(`Completed in ${formatDuration(result.elapsedMs)}\n`);
303326
}
304327
}

0 commit comments

Comments
 (0)