Back to Activepieces

Structured Logging

docs/handbook/engineering/playbooks/structured-logging.mdx

0.86.03.6 KB
Original Source

All structured logging on the server goes through evloglogger.{info,warn,error,debug}({ fields }, msg) and wideEvent.set/error/timed from @activepieces/server-utils.

The field keys (not the message string) are the real schema — they are what dashboards, alerts, and the OTLP drain query against. If the same thing is logged under different keys (runId here, flowRunId there, a bare id elsewhere), every query becomes a guessing game and correlation silently breaks.

One rule above all: one concept = one field, everywhere.

Group fields by entity

Following evlog's guidance, group related fields under the entity they belong to instead of flat, prefixed keys. An entity's own id is just id inside its group.

ts
// ✅ Grouped — clear, and flattens to flowRun.id / flow.id
logger.info({
  flowRun: { id: run.id, status: run.status },
  flow: { id: run.flowId },
  project: { id: run.projectId },
}, 'Flow run started')

// ❌ Flat prefixes, alias, and a bare id
logger.info({ runId: run.id, flowRunId: run.id, id: run.id }, 'Flow run started')

The group name is the camelCase singular entity from the domain model. The id lives at entity.id, and the entity's attributes sit beside it in the same group.

Rules

<Steps> <Step title="The id is `id`, inside its entity group"> Never a top-level `flowRunId`, `runId`, or bare `id`. A flow run is `flowRun: { id }`, a flow is `flow: { id }`. This gives exactly one queryable path per entity. </Step> <Step title="Attributes live beside the id, merged into one group"> `{ jobId, jobType }` becomes `job: { id, type }`; `{ pieceName, pieceVersion }` becomes `piece: { name, version }`. Never a bare `name` / `version` / `status` / `type` at the top level — they're meaningless without their entity. </Step> <Step title="Errors use `error`"> Pass the error straight to evlog — `logger.error(err)` or `logger.error({ err }, 'msg')` — and it lands under the canonical `error` key. Descriptive error fields like `migrationError` are fine. </Step> <Step title="Units go in the suffix"> Durations end in `Ms` (`durationMs`, `timings.{op}Ms`), bytes in `Bytes`, counts in `Count` or a plural. </Step> <Step title="Keep it shallow, and leave reserved fields flat"> One level of grouping (two max). `requestId`, `service`, `version`, `level`, `msg`, `timestamp`, `error`, `method`, `path` are added automatically — never set them, and never fold `requestId` into a group. </Step> </Steps>

Groups

GroupFieldsEntity
flowRunid, status, environmentA flow execution
flowid, versionA flow definition
flowVersionidA flow version
projectidA project
platformidA platform / workspace
useridA user
jobid, typeA queue job
piecename, versionAn integration piece
connectionidAn app connection
sandboxidAn execution sandbox
workeridA worker process
webhookid, requestId, mode, flowFound, responseStatusA webhook request
conversationidA chat conversation
waitpointidA pause / resume point
stepnameA flow step
triggernameA flow trigger
migrationnameA database migration
<Warning> This convention governs the keys in **logging calls** only, not entity or DTO field names. Data-model fields such as `JobData.runId`, DB query arguments, and service-call arguments stay as-is — the value is simply logged under `flowRun: { id }`. </Warning>