Skip to content
Merged
2 changes: 2 additions & 0 deletions docs/src/content/docs/commands/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ sentry log list <project>
| `-n, --limit <n>` | Number of log entries to show (1-1000, default: 100) |
| `-q, --query <query>` | Filter query (Sentry search syntax) |
| `-f, --follow [interval]` | Stream logs in real-time (optional: poll interval in seconds, default: 2) |
| `-t, --period <period>` | Time period (e.g., "30d", "14d", "24h"). Default: 30d. Log retention is 30 days. |
| `-s, --sort <order>` | Sort order: "newest" (default) or "oldest" |
| `--json` | Output as JSON |

**Examples:**
Expand Down
3 changes: 2 additions & 1 deletion plugins/sentry-cli/skills/sentry-cli/references/logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ List logs from a project
- `-n, --limit <value> - Number of log entries (1-1000) - (default: "100")`
- `-q, --query <value> - Filter query (Sentry search syntax)`
- `-f, --follow <value> - Stream logs (optionally specify poll interval in seconds)`
- `-t, --period <value> - Time period (e.g., "90d", "14d", "24h"). Default: 90d (project mode), 14d (trace mode)`
- `-t, --period <value> - Time period (e.g., "30d", "14d", "24h"). Default: 30d (project mode), 14d (trace mode)`
- `-s, --sort <value> - Sort order: "newest" (default) or "oldest" - (default: "newest")`
- `--fresh - Bypass cache, re-detect projects, and fetch fresh data`

**Examples:**
Expand Down
1 change: 1 addition & 0 deletions plugins/sentry-cli/skills/sentry-cli/references/traces.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ View logs associated with a trace
- `-t, --period <value> - Time period to search (e.g., "14d", "7d", "24h"). Default: 14d - (default: "14d")`
- `-n, --limit <value> - Number of log entries (<=1000) - (default: "100")`
- `-q, --query <value> - Additional filter query (Sentry search syntax)`
- `-s, --sort <value> - Sort order: "newest" (default) or "oldest" - (default: "newest")`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`

All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.
48 changes: 35 additions & 13 deletions src/commands/log/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@
// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
import * as Sentry from "@sentry/node-core/light";
import type { SentryContext } from "../../context.js";
import { listLogs, listTraceLogs } from "../../lib/api-client.js";
import { validateLimit } from "../../lib/arg-parsing.js";
import { AuthError, stringifyUnknown } from "../../lib/errors.js";
import {
type LogSortDirection,
listLogs,
listTraceLogs,
} from "../../lib/api-client.js";
import { parseLogSort, validateLimit } from "../../lib/arg-parsing.js";
import {
AuthError,
stringifyUnknown,
ValidationError,
} from "../../lib/errors.js";
import {
buildLogRowCells,
createLogStreamingTable,
Expand Down Expand Up @@ -46,6 +54,7 @@ type ListFlags = {
readonly query?: string;
readonly follow?: number;
readonly period?: string;
readonly sort: LogSortDirection;
readonly json: boolean;
readonly fresh: boolean;
readonly fields?: string[];
Expand Down Expand Up @@ -154,8 +163,10 @@ function parseLogListArgs(
return parseDualModeArgs(args, TRACE_USAGE_HINT);
}

/** Default time period for project-scoped log queries */
const DEFAULT_PROJECT_PERIOD = "90d";
/** Default time period for project-scoped log queries.
* Log retention is 30 days (https://docs.sentry.io/security-legal-pii/security/data-retention-periods/).
* Periods >30d hit a degraded API path that returns stale/incomplete data. */
const DEFAULT_PROJECT_PERIOD = "30d";

/**
* Execute a single fetch of logs (non-streaming mode).
Expand All @@ -173,21 +184,19 @@ async function executeSingleFetch(
query: flags.query,
limit: flags.limit,
statsPeriod: period,
sort: flags.sort,
});

if (logs.length === 0) {
return { result: { logs: [], hasMore: false }, hint: "No logs found." };
}

// Reverse for chronological order (API returns newest first, tail shows oldest first)
const chronological = [...logs].reverse();

const hasMore = logs.length >= flags.limit;
const countText = `Showing ${logs.length} log${logs.length === 1 ? "" : "s"}.`;
const tip = hasMore ? " Use --limit to show more, or -f to follow." : "";

return {
result: { logs: chronological, hasMore },
result: { logs, hasMore },
hint: `${countText}${tip}`,
};
}
Expand Down Expand Up @@ -433,6 +442,7 @@ async function executeTraceSingleFetch(
query: flags.query,
limit: flags.limit,
statsPeriod: period,
sort: flags.sort,
});

if (logs.length === 0) {
Expand All @@ -444,14 +454,12 @@ async function executeTraceSingleFetch(
};
}

const chronological = [...logs].reverse();

const hasMore = logs.length >= flags.limit;
const countText = `Showing ${logs.length} log${logs.length === 1 ? "" : "s"} for trace ${traceId}.`;
const tip = hasMore ? " Use --limit to show more." : "";

return {
result: { logs: chronological, traceId, hasMore },
result: { logs, traceId, hasMore },
hint: `${countText}${tip}`,
};
}
Expand Down Expand Up @@ -633,18 +641,32 @@ export const listCommand = buildListCommand(
kind: "parsed",
parse: String,
brief:
'Time period (e.g., "90d", "14d", "24h"). Default: 90d (project mode), 14d (trace mode)',
'Time period (e.g., "30d", "14d", "24h"). Default: 30d (project mode), 14d (trace mode)',
optional: true,
},
sort: {
kind: "parsed",
parse: parseLogSort,
brief: 'Sort order: "newest" (default) or "oldest"',
default: "newest",
},
},
aliases: {
n: "limit",
q: "query",
f: "follow",
t: "period",
s: "sort",
},
},
async *func(this: SentryContext, flags: ListFlags, ...args: string[]) {
if (flags.follow && flags.sort === "oldest") {
throw new ValidationError(
'--sort "oldest" cannot be used with --follow. Follow mode streams new logs as they arrive.',
"sort"
);
}

const { cwd } = this;

const parsed = parseLogListArgs(args);
Expand Down
19 changes: 13 additions & 6 deletions src/commands/trace/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
*/

import type { SentryContext } from "../../context.js";
import { listTraceLogs } from "../../lib/api-client.js";
import { validateLimit } from "../../lib/arg-parsing.js";
import { type LogSortDirection, listTraceLogs } from "../../lib/api-client.js";
import { parseLogSort, validateLimit } from "../../lib/arg-parsing.js";
import { openInBrowser } from "../../lib/browser.js";
import { buildCommand } from "../../lib/command.js";
import { filterFields } from "../../lib/formatters/json.js";
Expand All @@ -31,6 +31,7 @@ type LogsFlags = {
readonly period: string;
readonly limit: number;
readonly query?: string;
readonly sort: LogSortDirection;
readonly fresh: boolean;
readonly fields?: string[];
};
Expand Down Expand Up @@ -147,6 +148,12 @@ export const logsCommand = buildCommand({
brief: "Additional filter query (Sentry search syntax)",
optional: true,
},
sort: {
kind: "parsed",
parse: parseLogSort,
brief: 'Sort order: "newest" (default) or "oldest"',
default: "newest",
},
fresh: FRESH_FLAG,
},
aliases: {
Expand All @@ -155,6 +162,7 @@ export const logsCommand = buildCommand({
t: "period",
n: "limit",
q: "query",
s: "sort",
},
},
async *func(this: SentryContext, flags: LogsFlags, ...args: string[]) {
Expand All @@ -180,19 +188,18 @@ export const logsCommand = buildCommand({
statsPeriod: flags.period,
limit: flags.limit,
query: flags.query,
sort: flags.sort,
})
);

// Reverse to chronological order (API returns newest-first)
const chronological = [...logs].reverse();
const hasMore = chronological.length >= flags.limit;
const hasMore = logs.length >= flags.limit;

const emptyMessage =
`No logs found for trace ${traceId} in the last ${flags.period}.\n\n` +
`Try a longer period: sentry trace logs --period 30d ${traceId}`;

return yield new CommandOutput({
logs: chronological,
logs,
traceId,
hasMore,
emptyMessage,
Expand Down
1 change: 1 addition & 0 deletions src/lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export {
} from "./api/issues.js";
export {
getLogs,
type LogSortDirection,
listLogs,
listTraceLogs,
} from "./api/logs.js";
Expand Down
28 changes: 22 additions & 6 deletions src/lib/api/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ import {
unwrapResult,
} from "./infrastructure.js";

/** Sort direction for log queries: newest-first or oldest-first. */
export type LogSortDirection = "newest" | "oldest";

/** Map CLI sort direction to Sentry API sort parameter. */
function toApiSort(sort: LogSortDirection | undefined): string {
return sort === "oldest" ? "timestamp" : "-timestamp";
}

/** Fields to request from the logs API */
const LOG_FIELDS = [
"sentry.item_id",
Expand All @@ -41,8 +49,14 @@ type ListLogsOptions = {
query?: string;
/** Maximum number of log entries to return */
limit?: number;
/** Time period for logs (e.g., "90d", "10m") */
/**
* Time period for logs (e.g., "30d", "14d", "10m").
* Defaults to "30d" — the maximum log retention period.
* Periods >30d hit a degraded API path returning stale/incomplete data.
*/
statsPeriod?: string;
/** Sort direction: "newest" (default) or "oldest" */
sort?: LogSortDirection;
/** Only return logs after this timestamp_precise value (for streaming) */
afterTimestamp?: number;
};
Expand Down Expand Up @@ -83,8 +97,8 @@ export async function listLogs(
project: isNumericProject ? [Number(projectSlug)] : undefined,
query: fullQuery || undefined,
per_page: options.limit || API_MAX_PER_PAGE,
statsPeriod: options.statsPeriod ?? "7d",
sort: "-timestamp",
statsPeriod: options.statsPeriod ?? "30d",
sort: toApiSort(options.sort),
},
});

Expand Down Expand Up @@ -193,6 +207,8 @@ type ListTraceLogsOptions = {
* logs exist for the trace. Defaults to "14d".
*/
statsPeriod?: string;
/** Sort direction: "newest" (default) or "oldest" */
sort?: LogSortDirection;
};

/**
Expand All @@ -208,8 +224,8 @@ type ListTraceLogsOptions = {
*
* @param orgSlug - Organization slug
* @param traceId - The 32-character hex trace ID
* @param options - Optional query/limit/statsPeriod overrides
* @returns Array of trace log entries, ordered newest-first
* @param options - Optional query/limit/statsPeriod/sort overrides
* @returns Array of trace log entries
*/
export async function listTraceLogs(
orgSlug: string,
Expand All @@ -227,7 +243,7 @@ export async function listTraceLogs(
statsPeriod: options.statsPeriod ?? "14d",
per_page: options.limit ?? API_MAX_PER_PAGE,
query: options.query,
sort: "-timestamp",
sort: toApiSort(options.sort),
},
schema: TraceLogsResponseSchema,
}
Expand Down
23 changes: 23 additions & 0 deletions src/lib/arg-parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* project list) and single-item commands (issue view, explain, plan).
*/

import type { LogSortDirection } from "./api/logs.js";
import { ContextError, ValidationError } from "./errors.js";
import { validateResourceId } from "./input-validation.js";
import { logger } from "./logger.js";
Expand Down Expand Up @@ -261,6 +262,28 @@ export function validateLimit(value: string, min = 1, max = 1000): number {
return num;
}

// ---------------------------------------------------------------------------
// Log sort direction parsing (shared by log list, trace logs)
// ---------------------------------------------------------------------------

const VALID_LOG_SORT_DIRECTIONS: readonly LogSortDirection[] = [
"newest",
"oldest",
];

/**
* Parse --sort flag value for log commands.
* @throws Error if value is not "newest" or "oldest"
*/
export function parseLogSort(value: string): LogSortDirection {
if (!VALID_LOG_SORT_DIRECTIONS.includes(value as LogSortDirection)) {
throw new Error(
`Invalid sort value. Must be one of: ${VALID_LOG_SORT_DIRECTIONS.join(", ")}`
);
}
return value as LogSortDirection;
}

/** Default span depth when no value is provided */
const DEFAULT_SPAN_DEPTH = 3;

Expand Down
Loading
Loading