What exists
There is no tracing and there are no custom metrics today.
initTelemetry(serviceName) is called first thing in server.ts and returns immediately. It marks the place where an SDK would be initialized.
Request ids
requestIdMiddleware runs right after helmet and cors.
- If the request has a non empty
x-request-idheader, that value is used. A caller can pass its own id to correlate across services. - Otherwise a ULID is generated.
- The id is stored on
req.requestId, returned in theX-Request-Idresponse header, included in the response envelope, and attached to the access log line.
requestId from the response body. It finds the access log line and the [error] or [client-error] line for that request.
The pino logger
createLogger(serviceName) returns a pino logger with:
base: { service: serviceName }on every line.- ISO timestamps.
- Redaction with
remove: true, so redacted paths are dropped from the output, not masked.
err so pino serializes its type, message and stack.
core-api creates one logger in server.ts and puts it on AppContext as ctx.logger. Services receive it as a dependency, usually optional (logger?: Logger), so they run in tests without one.
Redacted paths
req and res shape. It does not find a token inside a field with another name. Do not log request bodies, API keys, OTP codes or session tokens under any key.
HTTP access log
httpLogger is a pino-http instance with its own logger named http.
Only three headers are serialized, so signatures, nonces, API keys and cookies do not reach the access log regardless of the redaction list.
Successful requests log at
debug. With the default pino level of info, the access log shows only 4xx and 5xx. createLogger does not set a level and nothing in the repo reads a log level variable, so seeing successful requests means changing the logger options in code.
url includes the query string. The webhook routes carry their token as ?token=..., so a webhook request that ends in 4xx or 5xx writes that token to the log. Treat access logs as sensitive.
Error logs
The error middleware in@perform/errors writes with console, not pino:
console.error and console.warn calls, so they are not JSON lines and carry no service field. They do not include a stack. For a stack, log at the point of failure with ctx.logger.error({ err }, '...').
The automation lane logs unexpected errors as console.error('[automation-api]', err).
@repo/logs
The ported packages (auth, api, payments, mail, storage, notifications, database) use a second logger built on consola:
Note the argument order is the opposite of pino. consola takes the message first.
@repo/database’s default client forwards Prisma events to this logger. The client core-api builds through createPrismaClient({ logger: log }) forwards them to pino instead.
Which one to use
The output of one process therefore mixes three formats: pino JSON, consola lines and raw
console lines from the error middleware. Log shipping has to tolerate non JSON lines.
Prisma logging
createPrismaClient subscribes to Prisma’s warn and error events and filters them before logging:
- Transient connection errors (
P1001,P1002,P1008,P1017, or a message that looks like a dropped connection) are logged atdebug. The client retries them up to four times with delays of 200, 500, 1000 and 2000 ms. Unique constraint failedis logged atdebug. Code that relies on a unique constraint to dedup, such as flow run starts, would otherwise fill the error log.- A pool error on an idle client is logged at
erroraspostgres pool error (idle client). - Query logging is off unless
logQueries: trueis passed, in which case queries are logged atdebugwith their duration.
Health and readiness
ReadinessCheck: { name, check, optional? }. A check that throws counts as failed. database is required and redis is optional. See Bootstrap and AppContext.
A degraded Redis means queues, OTP, replay protection and caches are affected while the API still answers. Signed lanes will fail, because the nonce store needs Redis.
Metrics
GET /metrics serves the Prometheus text format from a dedicated Registry with collectDefaultMetrics: process CPU, memory, event loop lag, garbage collection and handles. The registry is exported as metricsRegistry, so a module can register its own counter or histogram on it.
The endpoint has no authentication and is not rate limited. Restrict it at the proxy if the API is public.
Worker and job logs
Workers log a summary per run. Message strings are stable, so they can be used as search terms.
A run that did nothing logs at
debug, so a quiet system produces a quiet log at the default level.
Process level
unhandledRejectionis logged aterrorwith the reason and the process keeps running.uncaughtExceptionis logged and the process exits with code 1. The supervisor restarts it.core-api listeningis logged with the port at startup.shutdown initiatedis logged with the signal, andhttp server closedwhen the listener closes.