Skip to content

feat: add pino logger adapter - #225

Merged
GiovaniGuizzo merged 3 commits into
sidequestjs:masterfrom
bhupinder759:feat/custom-logger
Oct 14, 2026
Merged

GiovaniGuizzo merged 3 commits into
sidequestjs:masterfrom
bhupinder759:feat/custom-logger

Conversation

@bhupinder759

Copy link
Copy Markdown
Contributor

Closes #123

Checklist for Pull Requests

  • All tests pass (yarn test:all and yarn test:integration): unit tests and the non-DB integration tests pass locally; the backend and Postgres suites need Docker, which I don't have, so I'm relying on CI for those
  • Code follows the style guide and passes lint checks
  • Documentation is updated (README, docs, etc)
  • Linked to corresponding issue, if applicable

Summary of Changes

Adds logger.adapter ("winston" | "pino") and logger.options, as discussed in #123:

await Sidequest.start({
  logger: { level: "info", adapter: "pino", options: { base: { service: "jobs" } } },
});

Design

  • SidequestLogger (error/warn/info/debug(message, meta?)) is the strategy interface. createPinoLogger is the pino strategy, and loggerAdapters maps names to factories, so a new adapter is one factory plus one entry.
  • Winston stays the internal logger. When an adapter is selected, its only transport forwards each entry (message, scope, metadata) to that strategy. logger(), the Logger type and all call sites are unchanged, and the default output is the same as before.
  • The adapter is created in Engine.configure(), which runs in the app process, the forked engine process and each worker thread, so only serializable config crosses those boundaries. logger.options that can't be serialized (e.g. a timestamp function) fail at start instead of failing every job.
  • pino is an optional peer dependency of @sidequest/core, imported only when selected, with an install hint if it's missing. Error values in metadata go through pino.stdSerializers.err.

Small behaviour changes

  • The worker logs "Starting worker with provided configuration..." after engine.configure(), and validateConfig() runs after the logger is set up, so those lines also go through the selected adapter (and follow logger.level / logger.json).
  • Engine.configure() registers the backend right after the config, before the new await, so a concurrent caller never sees the config without a backend.

Docs: installation ("Logger Adapters", with the warning that pino must be installed), logging guide ("Using pino"), configuration table.

How I tested

  • yarn build, yarn lint, yarn format:check: pass.
  • Unit tests per package: core 110 passed, engine 281 passed / 1 skipped, sidequest 62 passed.
  • New tests/integration/pino-logger.integration.test.mjs (SQLite, no DB service): runs an app with adapter: "pino" in a child process and checks that every stdout line is pino JSON, including lines from the forked engine and a job's logger() call in a worker thread. It fails with the previous workers/main.ts order.
  • Manually: default output unchanged; fork: false + runner: "inline" works.

Add `logger.adapter` ("winston" | "pino") and `logger.options`. Each
adapter is a strategy that receives Sidequest's entries: winston stays the
internal logger and, when an adapter is selected, forwards every entry
to it through a stream transport, so `logger()` and its call sites do not
change. Winston remains the default.

The engine loads the adapter in Engine.configure(), which runs in the app
process, the forked engine process and every worker thread, so only
serializable config crosses those boundaries. Non-serializable
`logger.options` fail fast instead of failing every job.

pino is an optional peer dependency, imported only when selected. Error
values in the metadata are written with pino's `err` serializer.

The worker logs "Starting worker..." after configuring the engine, and
config validation runs after the logger is set up, so those entries also
go through the selected adapter.

Closes sidequestjs#123
@GiovaniGuizzo

Copy link
Copy Markdown
Contributor

Thanks for the contribution, @bhupinder759! I want to check this manually too today when I have the time.

Comment thread packages/core/src/logger.ts Resolved
…ries

pino writes to stdout asynchronously by default. A piscina worker thread
blocks its event loop while it waits for the next job, so the write never
completes and every later entry from that thread is lost: only the first
line per thread reached stdout. Without a transport, use
pino.destination({ dest: 1, sync: true }).

The integration test now runs two jobs on one worker thread and fails
without this change. The docs no longer recommend pino transports with
the thread runner, since they need the thread's event loop as well.
@GiovaniGuizzo
GiovaniGuizzo merged commit ab17f4b into sidequestjs:master Oct 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ability to switch default winston logger with a user-defined logger

3 participants