improvement(logs): show Redacting status while PII masking runs (#5855)

* improvement(logs): show Redacting status while log-persist PII masking runs

Log-stage PII redaction happens at persist time and can take minutes on large
payloads, during which the Logs page showed the run as Running long after
execution finished. The persist path now flips the log row to 'redacting'
(guarded on 'running' so a concurrent cancellation is never clobbered) right
before the masking work starts — only when the logs redaction stage is
actually enabled — and the terminal update overwrites it with the final
status. The Logs UI renders an amber non-filterable Redacting badge (row +
details sidebar via the shared STATUS_CONFIG), keeps polling the detail query
during the phase, and keeps resolving live progress markers. No migration:
status is a free-text column, and the contract already types it as string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

* fix(logs): never let the cosmetic redacting write abort log finalization

Review finding: the status flip was awaited without failure isolation, so a
transient DB error there rejected applyPiiRedaction before masking and the
terminal update never ran. The write is display-only; catch and warn instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1JYstmLHk9qMGyBDqYRcJ

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Theodore Li
2026-07-22 16:43:09 -04:00
committed by GitHub
co-authored by Claude Fable 5
parent fad9728cbd
commit b49fe16dae
4 changed files with 44 additions and 4 deletions
@@ -302,7 +302,9 @@ export default function Logs() {
(query: { state: { data?: WorkflowLogDetail } }) => {
if (!isLive) return false
const status = query.state.data?.status
return status === 'running' || status === 'pending' ? ACTIVE_RUN_DETAIL_REFRESH_MS : false
return status === 'running' || status === 'pending' || status === 'redacting'
? ACTIVE_RUN_DETAIL_REFRESH_MS
: false
},
[isLive]
)
@@ -18,7 +18,14 @@ export const LOG_COLUMNS = {
export const DELETED_WORKFLOW_LABEL = 'Deleted Workflow'
export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled' | 'cancelling'
export type LogStatus =
| 'error'
| 'pending'
| 'running'
| 'redacting'
| 'info'
| 'cancelled'
| 'cancelling'
/**
* Maps raw status string to LogStatus for display.
@@ -29,6 +36,8 @@ export function getDisplayStatus(status: string | null | undefined): LogStatus {
switch (status) {
case 'running':
return 'running'
case 'redacting':
return 'redacting'
case 'pending':
return 'pending'
case 'cancelling':
@@ -55,6 +64,7 @@ export const STATUS_CONFIG: Record<
error: { variant: 'red', label: 'Error', color: 'var(--text-error)', filterable: true },
pending: { variant: 'amber', label: 'Pending', color: '#f59e0b', filterable: true },
running: { variant: 'amber', label: 'Running', color: '#f59e0b', filterable: true },
redacting: { variant: 'amber', label: 'Redacting', color: '#f59e0b', filterable: false },
cancelling: { variant: 'amber', label: 'Cancelling...', color: '#f59e0b', filterable: false },
cancelled: { variant: 'orange', label: 'Cancelled', color: '#f97316', filterable: true },
info: {
+29 -1
View File
@@ -645,7 +645,8 @@ export class ExecutionLogger implements IExecutionLoggerService {
private async applyPiiRedaction(
workspaceId: string | null,
payload: RedactablePayload,
storeContext: { workflowId?: string | null; executionId: string; userId?: string | null }
storeContext: { workflowId?: string | null; executionId: string; userId?: string | null },
onRedactionStart?: () => Promise<void>
): Promise<RedactablePayload> {
if (!workspaceId) return payload
@@ -666,6 +667,10 @@ export class ExecutionLogger implements IExecutionLoggerService {
const config = resolveEffectivePiiRedaction({ orgSettings: row.orgSettings, workspaceId }).logs
if (!config.enabled) return payload
// Masking large payloads can take a while; let the caller surface the phase
// (e.g. flip the log row to 'redacting') before the slow work starts.
await onRedactionStart?.()
// The string redactor can't reach values already offloaded to large-value
// storage (>8MB refs). Always hydrate → mask → re-store them under the LOGS
// policy, even if the block-output stage already masked before offload: that
@@ -867,6 +872,29 @@ export class ExecutionLogger implements IExecutionLoggerService {
workflowId: existingLog?.workflowId ?? null,
executionId,
userId: actorUserId,
},
async () => {
// Execution is done but the log payload is still being masked — surface
// that as 'redacting' so the Logs UI doesn't show a stale 'running'.
// Guarded on 'running' so a concurrent cancellation is never clobbered;
// the terminal update below overwrites with the final status either way.
// Purely cosmetic: a failed write must never abort masking/finalization.
try {
await db
.update(workflowExecutionLogs)
.set({ status: 'redacting' })
.where(
and(
eq(workflowExecutionLogs.executionId, executionId),
eq(workflowExecutionLogs.status, 'running')
)
)
} catch (error) {
logger.warn('Failed to set redacting status on execution log', {
executionId,
error: getErrorMessage(error),
})
}
}
)
+1 -1
View File
@@ -176,7 +176,7 @@ export async function fetchLogDetail({
)
const liveMarkers =
log.status === 'running' || log.status === 'pending'
log.status === 'running' || log.status === 'pending' || log.status === 'redacting'
? ((await getProgressMarkers(log.executionId)) ?? {})
: {}
const rowMarkers = (executionData ?? {}) as ExecutionProgressMarkers