Core API

The API writes structured JSON to stdout with pino. One line is one event. There are two loggers in the process.

@perform/logger (pino)

createLogger(serviceName) in backend/packages/logger/src/index.ts returns a pino logger with:
  • a service field on every line (core-api for the app logger, http for the access logger),
  • ISO timestamps,
  • a redaction list whose matching fields are removed from the output.
Call it with a fields object first and the message second:
Pass errors under the err key so pino serialises the stack. The logger is available to every module as ctx.logger. pino writes numeric levels. 30 is info, 40 is warn, 50 is error. No level is configured, so the default applies and debug lines are not written.

The HTTP access log

httpLogger is a pino-http middleware mounted in app.ts after the body parsers. Successful requests are logged at debug, which the default level drops. In practice the access log shows only failed requests. That keeps volume low, and it means you cannot count traffic from the logs. Because the serialiser copies only three headers, authorization headers, cookies and API keys never reach the log through the access logger.

Redaction

These paths are removed from any log object that contains them:
Redaction matches exact paths. A secret logged under a different key, for example { apiKey } at the top level, is written in clear. Do not log request bodies, tokens, connection strings or message contents. Log ids.
With TRAINEE_OTP_DEV_MODE=true, the API logs every login code together with the phone number. That is what the flag is for. It must never be on in production.

@repo/logs (consola)

The packages ported from the web repository (@repo/auth, @repo/api, @repo/payments, @repo/mail, @repo/database) log through a consola logger from @repo/logs. Its output is human-readable text, not JSON, and it has no redaction list. The Hono app also logs one line per /api request through this logger. Expect a mix of JSON lines and plain text lines in the same stream. A log pipeline that parses JSON must tolerate non-JSON lines.

Prisma logging

The Prisma client forwards its warn and error events to the logger it was created with. Transient connection errors and expected unique constraint violations are downgraded to debug, so they do not appear at the default level. Query logging exists behind the logQueries option of createPrismaClient and is off.

Where the output goes

pm2 does not rotate these files by itself. Install and configure log rotation on the host. An unrotated log can fill the disk, and a full disk makes uploads and extraction fail during a deploy. The repository contains no log shipping configuration. If logs are sent to a central store, that is set up on the host.

Reading logs

The last command finds every line for one request id.

Rules for backend code

  • console.log is a lint error in apps/ and the @perform/* packages. Only console.warn and console.error are allowed, and the logger is preferred.
  • Put identifiers in the fields object (studioId, clientId, jobId), not in the message string, so lines can be filtered.
  • Keep the message constant. 'whatsapp send failed' can be searched. A message with an id interpolated cannot.
  • Never write an empty catch. Log at warn or error with the err field, or rethrow.
  • Workers should log the queue name and the job id.

Web app

The web app logs to stdout and stderr of the Next.js server process. Shared packages use the consola logger from @repo/logs. There is no structured access log and no request id generation in the web app. The API generates the id. When a server action fails because the API refused the request, the server-side client throws a CoreApiError carrying status, code and optional details from the API’s envelope. Log those fields. They identify the failing rule far better than the message. When the workspace script starts the backend for you, the API’s output goes to a log file under /tmp instead of your terminal. tools/ensure-core-api.sh prints the last 40 lines of it if the API fails to start.

Mobile app

The mobile app has no remote logging or crash reporting service in its dependencies. During development, logs appear in the Metro terminal and the device logs. src/lib/api/config.ts prints the resolved API base URL once at startup in development builds, which is the first thing to check when the app cannot reach the API. For a production problem, the evidence is on the server: the trainee lane’s failed requests in the API log, and push delivery receipts processed by the push receipt worker.

What is not in place

Stated plainly so nobody assumes otherwise:
  • No distributed tracing. initTelemetry is an empty function.
  • No error tracking service in any of the three repositories.
  • No log shipping configuration in the repositories.
  • No access log for successful requests at the default level.