Skip to content

Commit 121fc10

Browse files
betegonclaude
andcommitted
feat(log): support custom attributes in log view via --fields
getLogs and getLogsBatch now accept extraFields, which are merged with the default DETAILED_LOG_FIELDS before the API request — same pattern as listLogs. formatLogDetails renders a Custom Attributes section for any fields that were explicitly requested. The KNOWN_DETAIL_FIELDS blocklist approach was avoided; instead extraFields flows through LogViewData so the formatter only shows what was asked for. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent c90d5c7 commit 121fc10

4 files changed

Lines changed: 62 additions & 16 deletions

File tree

src/commands/log/view.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,8 @@ type LogViewData = {
399399
logs: DetailedSentryLog[];
400400
/** Org slug — needed by human formatter for trace URLs, also useful context in JSON */
401401
orgSlug: string;
402+
/** Custom fields requested via --fields, passed to the detail formatter */
403+
extraFields?: string[];
402404
};
403405

404406
/**
@@ -416,7 +418,7 @@ function formatLogViewHuman(data: LogViewData): string {
416418
if (parts.length > 0) {
417419
parts.push("\n---\n");
418420
}
419-
parts.push(formatLogDetails(entry, data.orgSlug));
421+
parts.push(formatLogDetails(entry, data.orgSlug, data.extraFields));
420422
}
421423
return parts.join("\n");
422424
}
@@ -496,7 +498,12 @@ export const viewCommand = buildCommand({
496498
}
497499

498500
// Fetch all requested log entries
499-
const logs = await getLogs(target.org, target.project, logIds);
501+
const logs = await getLogs(
502+
target.org,
503+
target.project,
504+
logIds,
505+
flags.fields
506+
);
500507

501508
if (logs.length === 0) {
502509
throwNotFoundError(logIds, target.org, target.project);
@@ -508,7 +515,11 @@ export const viewCommand = buildCommand({
508515
? `Detected from ${target.detectedFrom}`
509516
: undefined;
510517

511-
yield new CommandOutput({ logs, orgSlug: target.org });
518+
yield new CommandOutput({
519+
logs,
520+
orgSlug: target.org,
521+
extraFields: flags.fields,
522+
});
512523
return { hint };
513524
},
514525
});

src/lib/api/logs.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,16 +162,24 @@ async function getLogsBatch(
162162
orgSlug: string,
163163
projectSlug: string,
164164
batchIds: string[],
165-
config: Awaited<ReturnType<typeof getOrgSdkConfig>>
165+
config: Awaited<ReturnType<typeof getOrgSdkConfig>>,
166+
extraFields?: string[]
166167
): Promise<DetailedSentryLog[]> {
167168
const query = `project:${projectSlug} sentry.item_id:[${batchIds.join(",")}]`;
168169

170+
const fields = extraFields?.length
171+
? [
172+
...DETAILED_LOG_FIELDS,
173+
...extraFields.filter((f) => !DETAILED_LOG_FIELDS.includes(f)),
174+
]
175+
: DETAILED_LOG_FIELDS;
176+
169177
const result = await queryExploreEventsInTableFormat({
170178
...config,
171179
path: { organization_id_or_slug: orgSlug },
172180
query: {
173181
dataset: "logs",
174-
field: DETAILED_LOG_FIELDS,
182+
field: fields,
175183
query,
176184
per_page: batchIds.length,
177185
statsPeriod: LOG_RETENTION_PERIOD,
@@ -199,13 +207,14 @@ async function getLogsBatch(
199207
export async function getLogs(
200208
orgSlug: string,
201209
projectSlug: string,
202-
logIds: string[]
210+
logIds: string[],
211+
extraFields?: string[]
203212
): Promise<DetailedSentryLog[]> {
204213
const config = await getOrgSdkConfig(orgSlug);
205214

206215
// Single batch — no splitting needed
207216
if (logIds.length <= API_MAX_PER_PAGE) {
208-
return getLogsBatch(orgSlug, projectSlug, logIds, config);
217+
return getLogsBatch(orgSlug, projectSlug, logIds, config, extraFields);
209218
}
210219

211220
// Split into batches of API_MAX_PER_PAGE and fetch in parallel
@@ -215,7 +224,9 @@ export async function getLogs(
215224
}
216225

217226
const results = await Promise.all(
218-
batches.map((batch) => getLogsBatch(orgSlug, projectSlug, batch, config))
227+
batches.map((batch) =>
228+
getLogsBatch(orgSlug, projectSlug, batch, config, extraFields)
229+
)
219230
);
220231

221232
return results.flat();

src/lib/formatters/log.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,14 @@ function formatSeverityLabel(severity: string | null | undefined): string {
287287
*
288288
* @param log - The detailed log entry to format
289289
* @param orgSlug - Organization slug for building trace URLs
290+
* @param extraFields - Custom fields requested via --fields, shown in Custom Attributes section
290291
* @returns Rendered terminal string
291292
*/
292293
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: log detail formatting requires multiple conditional sections
293294
export function formatLogDetails(
294295
log: DetailedSentryLog,
295-
orgSlug: string
296+
orgSlug: string,
297+
extraFields?: string[]
296298
): string {
297299
const logId = log["sentry.item_id"];
298300
const lines: string[] = [];
@@ -394,5 +396,16 @@ export function formatLogDetails(
394396
lines.push(mdKvTable(otelRows, "OpenTelemetry"));
395397
}
396398

399+
// Custom Attributes — fields explicitly requested via --fields
400+
if (extraFields?.length) {
401+
const customRows: [string, string][] = extraFields
402+
.filter((f) => log[f] !== null && log[f] !== undefined)
403+
.map((f) => [f, String(log[f])]);
404+
if (customRows.length > 0) {
405+
lines.push("");
406+
lines.push(mdKvTable(customRows, "Custom Attributes"));
407+
}
408+
}
409+
397410
return renderMarkdown(lines.join("\n"));
398411
}

test/commands/log/view.func.test.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,12 @@ describe("viewCommand.func", () => {
193193
);
194194

195195
// getLogs should have been called with both IDs
196-
expect(getLogsSpy).toHaveBeenCalledWith("my-org", "proj", [ID1, ID2]);
196+
expect(getLogsSpy).toHaveBeenCalledWith(
197+
"my-org",
198+
"proj",
199+
[ID1, ID2],
200+
undefined
201+
);
197202

198203
const output = stdoutWrite.mock.calls.map((c) => c[0]).join("");
199204
const parsed = JSON.parse(output);
@@ -295,9 +300,12 @@ describe("viewCommand.func", () => {
295300
await func.call(context, { json: true, web: false }, "my-project", ID1);
296301

297302
expect(resolveProjectBySlugSpy).toHaveBeenCalled();
298-
expect(getLogsSpy).toHaveBeenCalledWith("resolved-org", "resolved-proj", [
299-
ID1,
300-
]);
303+
expect(getLogsSpy).toHaveBeenCalledWith(
304+
"resolved-org",
305+
"resolved-proj",
306+
[ID1],
307+
undefined
308+
);
301309
});
302310

303311
test("org/ target (org-all) throws ContextError", async () => {
@@ -327,9 +335,12 @@ describe("viewCommand.func", () => {
327335
await func.call(context, { json: false, web: false }, ID1);
328336

329337
expect(resolveOrgAndProjectSpy).toHaveBeenCalled();
330-
expect(getLogsSpy).toHaveBeenCalledWith("detected-org", "detected-proj", [
331-
ID1,
332-
]);
338+
expect(getLogsSpy).toHaveBeenCalledWith(
339+
"detected-org",
340+
"detected-proj",
341+
[ID1],
342+
undefined
343+
);
333344

334345
// Human output should include the detected-from hint
335346
const output = stdoutWrite.mock.calls.map((c) => c[0]).join("");

0 commit comments

Comments
 (0)