Skip to content

Commit 0c4e288

Browse files
authored
feat: add job log command (#268)
* feat: add `job log` command to retrieve job execution logs New command to fetch and display job execution logs with three modes: - Specific execution: `b2c job log <jobId> <executionId>` - Most recent with log: `b2c job log <jobId>` - Most recent failed: `b2c job log <jobId> --failed` Includes ANSI syntax highlighting (shared with `b2c logs`) for timestamps and log levels when output is a TTY. * fix: add type casts for runSilent results in test to satisfy test tsconfig * fix: typecheck:agent now uses test tsconfig to match CI pretest
1 parent fe25ae6 commit 0c4e288

7 files changed

Lines changed: 408 additions & 5 deletions

File tree

.changeset/job-log-command.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@salesforce/b2c-cli': minor
3+
---
4+
5+
Add `job log` command to retrieve and display job execution logs. Supports fetching logs for a specific execution or automatically finding the most recent (or most recent failed) execution with a log file.

docs/cli/jobs.md

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ Configure these resources in Business Manager under **Administration** > **Site
1717
| Resource | Methods | Commands |
1818
|----------|---------|----------|
1919
| `/jobs/*/executions` | POST | `job run` |
20-
| `/jobs/*/executions/*` | GET | `job run --wait`, `job wait` |
21-
| `/job_execution_search` | POST | `job search` |
20+
| `/jobs/*/executions/*` | GET | `job run --wait`, `job wait`, `job log` |
21+
| `/job_execution_search` | POST | `job search`, `job log` |
2222

2323
### WebDAV Access
2424

25-
The `job import` and `job export` commands also require WebDAV access for file transfer.
25+
The `job import`, `job export`, and `job log` commands also require WebDAV access for file transfer.
2626

2727
### Configuration
2828

@@ -199,6 +199,59 @@ The command displays a table of job executions with:
199199

200200
---
201201

202+
## b2c job log
203+
204+
Retrieve the log for a job execution. When no execution ID is provided, the command finds the most recent execution that has a log file.
205+
206+
### Usage
207+
208+
```bash
209+
b2c job log JOBID [EXECUTIONID]
210+
```
211+
212+
### Arguments
213+
214+
| Argument | Description | Required |
215+
|----------|-------------|----------|
216+
| `JOBID` | Job ID | Yes |
217+
| `EXECUTIONID` | Execution ID (if omitted, finds the most recent execution with a log) | No |
218+
219+
### Flags
220+
221+
In addition to [global flags](./index#global-flags):
222+
223+
| Flag | Description | Default |
224+
|------|-------------|---------|
225+
| `--failed` | Find the most recent failed execution with a log | `false` |
226+
227+
### Examples
228+
229+
```bash
230+
# Get the most recent log for a job
231+
b2c job log my-custom-job
232+
233+
# Get the most recent failed log
234+
b2c job log my-custom-job --failed
235+
236+
# Get the log for a specific execution
237+
b2c job log my-custom-job abc123-def456
238+
239+
# Output as JSON (includes execution metadata and log content)
240+
b2c job log my-custom-job --json
241+
242+
# Pipe log to a file
243+
b2c job log my-custom-job > job.log
244+
```
245+
246+
### Notes
247+
248+
- Not all job executions produce log files. The command will skip executions without logs when searching.
249+
- Log content is written to stdout, making it easy to pipe to a file or other tools.
250+
- Status messages are written to stderr so they don't interfere with piped output.
251+
- The `job log` command requires WebDAV access to retrieve log files.
252+
253+
---
254+
202255
## b2c job import
203256

204257
Import a site archive to a B2C Commerce instance using the `sfcc-site-archive-import` system job.

packages/b2c-cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@
311311
"build": "shx rm -rf dist && tsc -p tsconfig.build.json",
312312
"lint": "eslint",
313313
"lint:agent": "eslint --quiet",
314-
"typecheck:agent": "tsc --noEmit --pretty false",
314+
"typecheck:agent": "tsc --noEmit -p test --pretty false",
315315
"format": "prettier --write src",
316316
"format:check": "prettier --check src",
317317
"postpack": "shx rm -f oclif.manifest.json",
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2
4+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
import {Args, Flags} from '@oclif/core';
7+
import {InstanceCommand} from '@salesforce/b2c-tooling-sdk/cli';
8+
import {
9+
searchJobExecutions,
10+
getJobExecution,
11+
getJobLog,
12+
type JobExecution,
13+
} from '@salesforce/b2c-tooling-sdk/operations/jobs';
14+
import {t, withDocs} from '../../i18n/index.js';
15+
import {highlightLogText} from '../../utils/logs/index.js';
16+
17+
interface JobLogResult {
18+
execution: JobExecution;
19+
log: string;
20+
}
21+
22+
export default class JobLog extends InstanceCommand<typeof JobLog> {
23+
static args = {
24+
jobId: Args.string({
25+
description: 'Job ID',
26+
required: true,
27+
}),
28+
executionId: Args.string({
29+
description: 'Execution ID (if omitted, finds the most recent execution with a log)',
30+
required: false,
31+
}),
32+
};
33+
34+
static description = withDocs(
35+
t('commands.job.log.description', 'Retrieve the log for a job execution'),
36+
'/cli/jobs.html#b2c-job-log',
37+
);
38+
39+
static enableJsonFlag = true;
40+
41+
static examples = [
42+
'<%= config.bin %> <%= command.id %> my-job',
43+
'<%= config.bin %> <%= command.id %> my-job --failed',
44+
'<%= config.bin %> <%= command.id %> my-job abc123-def456',
45+
'<%= config.bin %> <%= command.id %> my-job --json',
46+
];
47+
48+
static flags = {
49+
...InstanceCommand.baseFlags,
50+
failed: Flags.boolean({
51+
description: 'Find the most recent failed execution with a log',
52+
default: false,
53+
}),
54+
'no-color': Flags.boolean({
55+
description: 'Disable colored output',
56+
default: false,
57+
}),
58+
};
59+
60+
protected operations = {
61+
searchJobExecutions,
62+
getJobExecution,
63+
getJobLog,
64+
};
65+
66+
async run(): Promise<JobLogResult> {
67+
this.requireOAuthCredentials();
68+
69+
const {jobId, executionId} = this.args;
70+
const {failed} = this.flags;
71+
72+
let execution: JobExecution;
73+
74+
if (executionId) {
75+
this.log(
76+
t('commands.job.log.fetchingSpecific', 'Fetching log for job {{jobId}} execution {{executionId}}...', {
77+
jobId,
78+
executionId,
79+
}),
80+
);
81+
execution = await this.operations.getJobExecution(this.instance, jobId, executionId);
82+
} else {
83+
this.log(
84+
failed
85+
? t(
86+
'commands.job.log.searchingFailed',
87+
'Searching for most recent failed execution with log for job {{jobId}}...',
88+
{jobId},
89+
)
90+
: t('commands.job.log.searching', 'Searching for most recent execution with log for job {{jobId}}...', {
91+
jobId,
92+
}),
93+
);
94+
95+
const results = await this.operations.searchJobExecutions(this.instance, {
96+
jobId,
97+
status: failed ? ['ERROR'] : undefined,
98+
count: 10,
99+
sortBy: 'start_time',
100+
sortOrder: 'desc',
101+
});
102+
103+
const match = results.hits.find((hit) => hit.is_log_file_existing);
104+
if (!match) {
105+
const msg = failed
106+
? t(
107+
'commands.job.log.noFailedExecutionFound',
108+
'No failed execution with a log file found for job {{jobId}}',
109+
{jobId},
110+
)
111+
: t('commands.job.log.noExecutionFound', 'No execution with a log file found for job {{jobId}}', {
112+
jobId,
113+
});
114+
this.error(msg);
115+
}
116+
117+
execution = match;
118+
}
119+
120+
if (!execution.is_log_file_existing) {
121+
this.error(t('commands.job.log.noLogFile', 'No log file exists for this execution'));
122+
}
123+
124+
this.log(
125+
t('commands.job.log.foundExecution', 'Found execution {{executionId}} ({{status}})', {
126+
executionId: execution.id ?? 'unknown',
127+
status: execution.exit_status?.code || execution.execution_status || 'unknown',
128+
}),
129+
);
130+
131+
const log = await this.operations.getJobLog(this.instance, execution);
132+
133+
if (!this.jsonEnabled()) {
134+
const useColor = !this.flags['no-color'] && process.stdout.isTTY;
135+
process.stdout.write(useColor ? highlightLogText(log) : log);
136+
}
137+
138+
return {execution, log};
139+
}
140+
}

packages/b2c-cli/src/utils/logs/format.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,30 @@ export function formatEntry(entry: LogEntry, useColor: boolean): string {
6969
return `${header}\n${entry.message}\n`;
7070
}
7171

72+
/**
73+
* Matches a B2C log line start: [YYYY-MM-DD HH:MM:SS.mmm GMT] LEVEL ...
74+
*/
75+
const LOG_LINE_RE = /^(\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s+\w+\])\s+(ERROR|FATAL|WARN|INFO|DEBUG|TRACE)\b/;
76+
77+
/**
78+
* Applies ANSI highlighting to raw log text, line by line.
79+
* Timestamps are dimmed, log levels are colored to match `formatEntry` output.
80+
* Useful for job logs and other raw log content that hasn't been parsed into LogEntry objects.
81+
*/
82+
export function highlightLogText(text: string): string {
83+
return text
84+
.split('\n')
85+
.map((line) => {
86+
const match = LOG_LINE_RE.exec(line);
87+
if (!match) return line;
88+
const [, timestamp, level] = match;
89+
const color = LEVEL_COLORS[level] || '';
90+
const rest = line.slice(match[0].length);
91+
return `${DIM}${timestamp}${RESET} ${color}${level}${RESET}${rest}`;
92+
})
93+
.join('\n');
94+
}
95+
7296
/**
7397
* Sets up a path normalizer for IDE click-to-open functionality.
7498
* Priority: 1) explicit cartridgePath, 2) auto-discover cartridges, 3) undefined (no normalization)

0 commit comments

Comments
 (0)