logger.{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 justid inside its group.
entity.id, and the entity’s attributes sit beside it in the same group.
Rules
1
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.2
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.3
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.4
Units go in the suffix
Durations end in
Ms (durationMs, timings.{op}Ms), bytes in Bytes, counts in Count or a plural.5
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.