Skip to content

Commit 79dbf14

Browse files
authored
improvement(logs): log each admission refusal once per window and demote per-request success chatter to debug (#9722)
Admission gate warnings now share the blocked-run throttle with their error row, so a sender redelivering into a refusal logs once per window instead of on every attempt. The webhook processor no longer repeats the refusal preprocessing already logged. Per-request success-path lines (preprocessing steps, valid-token checks, billing intermediate steps, Slack trigger-filter skips, table upserts, trigger and execution-state selection, storage uploads) move to debug. The billing top-up line keeps the payer and model. PostHog upstream connection failures log at warn, and the idempotency failure warning uses a static message with fields.
1 parent 6d8c34f commit 79dbf14

14 files changed

Lines changed: 98 additions & 64 deletions

File tree

‎apps/sim/app/api/billing/update-cost/route.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ async function updateCostInner(req: NextRequest, span: Span): Promise<NextRespon
196196
const startTime = Date.now()
197197

198198
try {
199-
logger.info(`[${requestId}] Update cost request started`)
199+
logger.debug(`[${requestId}] Update cost request started`)
200200

201201
// Check authentication (internal API key)
202202
const authResult = checkInternalApiKey(req)
@@ -324,7 +324,7 @@ async function updateCostInner(req: NextRequest, span: Span): Promise<NextRespon
324324
...(idempotencyKey ? { [TraceAttr.BillingIdempotencyKey]: idempotencyKey } : {}),
325325
})
326326

327-
logger.info(`[${requestId}] Processing cost update`, {
327+
logger.debug(`[${requestId}] Processing cost update`, {
328328
userId,
329329
cost,
330330
model,
@@ -408,7 +408,7 @@ async function updateCostInner(req: NextRequest, span: Span): Promise<NextRespon
408408
}
409409
: undefined
410410

411-
logger.info(`[${requestId}] Billing payer resolved`, {
411+
logger.debug(`[${requestId}] Billing payer resolved`, {
412412
userId,
413413
billingProtocol: protocol,
414414
billingResolution: getBillingResolution(isMarkerlessLegacy, billingAttribution),
@@ -443,7 +443,10 @@ async function updateCostInner(req: NextRequest, span: Span): Promise<NextRespon
443443
const billed = result.billed
444444
logger.info(`[${requestId}] Cumulative cost top-up`, {
445445
userId,
446+
billingPayer: billingContext?.billingEntity ?? { type: 'user', id: userId },
447+
workspaceId: resolvedWorkspaceId,
446448
source,
449+
model,
447450
cumulativeCost: cost,
448451
billedDelta: result.delta,
449452
newTotal: result.total,

‎apps/sim/app/ingest/[[...path]]/route.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('PostHog ingest proxy', () => {
3131
const response = await POST(createIngestRequest(controller.signal))
3232

3333
expect(response.status).toBe(499)
34-
expect(logger.error).not.toHaveBeenCalled()
34+
expect(logger.warn).not.toHaveBeenCalled()
3535
})
3636

3737
it('reports an upstream failure while the client is still connected', async () => {
@@ -40,7 +40,7 @@ describe('PostHog ingest proxy', () => {
4040
const response = await POST(createIngestRequest())
4141

4242
expect(response.status).toBe(502)
43-
expect(logger.error).toHaveBeenCalledWith(
43+
expect(logger.warn).toHaveBeenCalledWith(
4444
'PostHog proxy error',
4545
expect.objectContaining({ error: 'The operation timed out.' })
4646
)

‎apps/sim/app/ingest/[[...path]]/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async function handler(request: NextRequest) {
6161
logger.info('Client closed PostHog proxy request', { method: request.method })
6262
return new NextResponse(null, { status: 499 })
6363
}
64-
logger.error('PostHog proxy error', {
64+
logger.warn('PostHog proxy error', {
6565
url,
6666
method: request.method,
6767
error: toError(error).message,

‎apps/sim/background/webhook-execution.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1100,7 +1100,7 @@ async function executeWebhookJobInternal(
11001100
}
11011101
}
11021102

1103-
logger.info(`[${requestId}] Executing workflow for ${payload.provider} webhook`)
1103+
logger.debug(`[${requestId}] Executing workflow for ${payload.provider} webhook`)
11041104

11051105
const metadata: ExecutionMetadata = {
11061106
requestId,

‎apps/sim/executor/handlers/trigger/handler.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class TriggerBlockHandler implements BlockHandler {
1515
block: SerializedBlock,
1616
inputs: Record<string, any>
1717
): Promise<any> {
18-
logger.info(`Executing trigger block: ${block.id} (Type: ${block.metadata?.id})`)
18+
logger.debug(`Executing trigger block: ${block.id} (Type: ${block.metadata?.id})`)
1919

2020
if (block.metadata?.id === BlockType.STARTER) {
2121
return this.executeStarterBlock(ctx, block, inputs)

‎apps/sim/lib/core/idempotency/service.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,10 @@ export class IdempotencyService {
714714
)
715715
}
716716

717-
logger.warn(`Operation failed: ${claimResult.normalizedKey} - ${errorMessage}`)
717+
logger.warn('Idempotent operation failed', {
718+
key: claimResult.normalizedKey,
719+
error: errorMessage,
720+
})
718721
throw error
719722
}
720723
}

‎apps/sim/lib/execution/preprocessing.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
billingUsageReservationMockFns,
1616
} from '@sim/testing/mocks/billing-usage-reservation.mock'
1717
import { executionLimitsMock } from '@sim/testing/mocks/execution-limits.mock'
18+
import { getMockLogger } from '@sim/testing/mocks/logger.mock'
1819
import { loggingSessionMockFns } from '@sim/testing/mocks/logging-session.mock'
1920
import { createMockRedis } from '@sim/testing/mocks/redis.mock'
2021
import { redisConfigMockFns, resetRedisConfigMock } from '@sim/testing/mocks/redis-config.mock'
@@ -1123,6 +1124,22 @@ describe('preprocessExecution admission rejection codes and blocked-run log thro
11231124
expect(loggingSessionMockFns.mockSafeCompleteWithError).toHaveBeenCalledTimes(1)
11241125
})
11251126

1127+
it('logs one warning for repeated refusals of a workflow by the same gate', async () => {
1128+
const workflowId = 'workflow-1'
1129+
const logger = getMockLogger('ExecutionPreprocessing')
1130+
logger.warn.mockClear()
1131+
1132+
for (let attempt = 0; attempt < 3; attempt++) {
1133+
await refuse(workflowId, { throttleErrorLogs: true })
1134+
}
1135+
1136+
expect(logger.warn).toHaveBeenCalledOnce()
1137+
expect(logger.warn).toHaveBeenCalledWith(
1138+
'Attributed usage gate blocked actor',
1139+
expect.objectContaining({ workflowId })
1140+
)
1141+
})
1142+
11261143
it('writes a row for a different gate refusing the same workflow inside the window', async () => {
11271144
const workflowId = 'workflow-1'
11281145
await refuse(workflowId, { throttleErrorLogs: true })

‎apps/sim/lib/execution/preprocessing.ts‎

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,12 @@ function startAdmissionReads(params: {
362362
}
363363
}
364364

365+
/** A gate refusal's warning, emitted alongside its error row so both share the throttle window. */
366+
interface GateWarning {
367+
message: string
368+
fields: Record<string, unknown>
369+
}
370+
365371
export async function preprocessExecution(
366372
options: PreprocessExecutionOptions
367373
): Promise<PreprocessExecutionResult> {
@@ -401,11 +407,15 @@ export async function preprocessExecution(
401407
const isFailureLogSuppressed = (failure: PreprocessExecutionError): boolean =>
402408
suppressRetryableFailureLogs && failure.statusCode >= 500 && failure.retryable === true
403409

404-
/** Records an admission gate's error row, at most once per gate and outcome per window when throttled. */
410+
/**
411+
* Records an admission gate's warning and error row, at most once per gate and outcome per window
412+
* when throttled, so a sender redelivering into a refusal does not log every attempt.
413+
*/
405414
const recordGateFailure = async (
406415
gate: 'ban' | 'usage' | 'rate-limit' | 'reservation',
407416
failure: PreprocessExecutionError,
408-
record: Parameters<typeof logPreprocessingError>[0]
417+
record: Parameters<typeof logPreprocessingError>[0],
418+
warning?: GateWarning
409419
): Promise<void> => {
410420
if (isFailureLogSuppressed(failure)) return
411421
if (
@@ -416,10 +426,12 @@ export async function preprocessExecution(
416426
) {
417427
return
418428
}
429+
if (warning)
430+
logger.warn(warning.message, { requestId, workflowId, triggerType, ...warning.fields })
419431
await recordPreprocessingError(record)
420432
}
421433

422-
logger.info(`[${requestId}] Starting execution preprocessing`, {
434+
logger.debug(`[${requestId}] Starting execution preprocessing`, {
423435
workflowId,
424436
userId,
425437
triggerType,
@@ -577,21 +589,21 @@ export async function preprocessExecution(
577589
}
578590
actorUserId = validatedAttribution.actorUserId
579591
billingAttribution = validatedAttribution
580-
logger.info(`[${requestId}] Reusing serialized billing attribution`, {
592+
logger.debug(`[${requestId}] Reusing serialized billing attribution`, {
581593
actorUserId,
582594
billingEntity: billingAttribution.billingEntity,
583595
})
584596
}
585597

586598
if (!actorUserId && useAuthenticatedUserAsActor && userId) {
587599
actorUserId = userId
588-
logger.info(`[${requestId}] Using authenticated user as actor: ${actorUserId}`)
600+
logger.debug(`[${requestId}] Using authenticated user as actor: ${actorUserId}`)
589601
}
590602

591603
if (!actorUserId) {
592604
billingAttribution = await admission.attributionFor(workspaceId)
593605
actorUserId = billingAttribution.actorUserId
594-
logger.info(`[${requestId}] Using atomically resolved system actor and payer`, {
606+
logger.debug(`[${requestId}] Using atomically resolved system actor and payer`, {
595607
actorUserId,
596608
billingEntity: billingAttribution.billingEntity,
597609
})
@@ -672,6 +684,7 @@ export async function preprocessExecution(
672684
interface GateFailure {
673685
response: PreprocessExecutionFailure
674686
recordError?: Parameters<typeof recordPreprocessingError>[0]
687+
warning?: GateWarning
675688
}
676689

677690
/** Usage figures captured by the read gate and reused by the atomic reservation. */
@@ -690,13 +703,8 @@ export async function preprocessExecution(
690703
try {
691704
const bannedUserIds = await gateReads.bannedUserIds
692705
if (bannedUserIds.length > 0) {
693-
logger.warn(`[${requestId}] Execution blocked: banned account`, {
694-
workflowId,
695-
bannedUserIds,
696-
triggerType,
697-
})
698-
699706
return {
707+
warning: { message: 'Execution blocked: banned account', fields: { bannedUserIds } },
700708
response: {
701709
success: false,
702710
error: {
@@ -774,16 +782,17 @@ export async function preprocessExecution(
774782
}
775783
: null
776784
if (usageCheck.isExceeded) {
777-
logger.warn(`[${requestId}] Attributed usage gate blocked actor ${actorUserId}.`, {
778-
currentUsage: snapshot?.currentUsage,
779-
limit: snapshot?.limit,
780-
scope: usageCheck.scope,
781-
workflowId,
782-
triggerType,
783-
})
784-
785785
return {
786786
failure: {
787+
warning: {
788+
message: 'Attributed usage gate blocked actor',
789+
fields: {
790+
actorUserId,
791+
currentUsage: snapshot?.currentUsage,
792+
limit: snapshot?.limit,
793+
scope: usageCheck.scope,
794+
},
795+
},
787796
response: {
788797
success: false,
789798
error: {
@@ -876,13 +885,11 @@ export async function preprocessExecution(
876885
)
877886

878887
if (!info.allowed) {
879-
logger.warn(`[${requestId}] Rate limit exceeded for user ${actorUserId}`, {
880-
triggerType,
881-
remaining: info.remaining,
882-
resetAt: info.resetAt,
883-
})
884-
885888
return {
889+
warning: {
890+
message: 'Rate limit exceeded',
891+
fields: { actorUserId, remaining: info.remaining, resetAt: info.resetAt },
892+
},
886893
response: {
887894
success: false,
888895
error: {
@@ -947,7 +954,8 @@ export async function preprocessExecution(
947954
await recordGateFailure(
948955
banFailure ? 'ban' : 'usage',
949956
readGateFailure.response.error,
950-
readGateFailure.recordError
957+
readGateFailure.recordError,
958+
readGateFailure.warning
951959
)
952960
}
953961
return readGateFailure.response
@@ -959,7 +967,8 @@ export async function preprocessExecution(
959967
await recordGateFailure(
960968
'rate-limit',
961969
rateLimitFailure.response.error,
962-
rateLimitFailure.recordError
970+
rateLimitFailure.recordError,
971+
rateLimitFailure.warning
963972
)
964973
}
965974
return rateLimitFailure.response
@@ -1009,12 +1018,6 @@ export async function preprocessExecution(
10091018
reservation.reason
10101019
)
10111020
const message = RESERVATION_DENIAL_MESSAGE[reservation.reason]
1012-
logger.warn(`[${requestId}] Admission reservation full for user ${actorUserId}`, {
1013-
workflowId,
1014-
triggerType,
1015-
constraint: reservation.reason,
1016-
})
1017-
10181021
const failure: PreprocessExecutionError = {
10191022
message,
10201023
statusCode: descriptor.statusCode,
@@ -1026,17 +1029,25 @@ export async function preprocessExecution(
10261029
constraint: reservation.reason,
10271030
},
10281031
}
1029-
await recordGateFailure('reservation', failure, {
1030-
workflowId,
1031-
executionId,
1032-
triggerType,
1033-
requestId,
1034-
userId: actorUserId,
1035-
workspaceId,
1036-
errorMessage: message,
1037-
loggingSession: providedLoggingSession,
1038-
triggerData,
1039-
})
1032+
await recordGateFailure(
1033+
'reservation',
1034+
failure,
1035+
{
1036+
workflowId,
1037+
executionId,
1038+
triggerType,
1039+
requestId,
1040+
userId: actorUserId,
1041+
workspaceId,
1042+
errorMessage: message,
1043+
loggingSession: providedLoggingSession,
1044+
triggerData,
1045+
},
1046+
{
1047+
message: 'Admission reservation full',
1048+
fields: { actorUserId, constraint: reservation.reason },
1049+
}
1050+
)
10401051

10411052
return { success: false, error: failure }
10421053
}
@@ -1068,7 +1079,7 @@ export async function preprocessExecution(
10681079
}
10691080
}
10701081

1071-
logger.info(`[${requestId}] All preprocessing checks passed`, {
1082+
logger.debug(`[${requestId}] All preprocessing checks passed`, {
10721083
workflowId,
10731084
actorUserId,
10741085
triggerType,

‎apps/sim/lib/oauth/credential-service.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,7 +1764,7 @@ export async function resolveCredentialTokenBundle(
17641764
logger.error(`[${requestId}] Missing access token for credential`)
17651765
return null
17661766
}
1767-
if (!result.refreshed) logger.info(`[${requestId}] Access token is valid for credential`)
1767+
if (!result.refreshed) logger.debug(`[${requestId}] Access token is valid for credential`)
17681768
return { accessToken: result.accessToken }
17691769
}
17701770

@@ -1817,7 +1817,7 @@ export async function refreshTokenIfNeeded(
18171817
throw new Error('OAuth access token expired and cannot be refreshed; reconnect the account')
18181818
}
18191819
if (result.kind === 'token') {
1820-
if (!result.refreshed) logger.info(`[${requestId}] Access token is valid`)
1820+
if (!result.refreshed) logger.debug(`[${requestId}] Access token is valid`)
18211821
/** A row storing neither token returns its empty token as before; callers decide how to fail. */
18221822
return {
18231823
accessToken: result.accessToken ?? credential.accessToken,

‎apps/sim/lib/table/rows/service.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -959,7 +959,7 @@ export async function upsertRow(
959959
}
960960
})
961961

962-
logger.info(
962+
logger.debug(
963963
`[${requestId}] Upserted (${result.operation}) row ${result.row.id} in table ${data.tableId}`
964964
)
965965

0 commit comments

Comments
 (0)