growi-loggerlogger.info(obj, msg)) — no call-site changes neededlogger.child() or custom serializers used in GROWI — simplifies migration significantly@opentelemetry/instrumentation-pino supports pino <10; need to verify v9.x or v10 compatibilitylogger[level]([mergingObject], [message], [...interpolationValues]) — same as bunyanname option adds a "name" field to JSON output, same as bunyanmsg is the default message key (same as bunyan), configurable via messageKeypino.child(bindings, options) works similarly to bunyan's child()logger.info('msg'), logger.info({obj}, 'msg'), logger.error(err) require no changesbrowser fieldconsole.error (fatal/error), console.warn, console.info, console.debug, console.tracebrowser.asObject: true outputs structured objectsbrowser.write allows custom per-level handlerslevel option)short (dev) and long (prod) output modessingleLine: true + ignore: 'pid,hostname'translateTime: 'SYS:standard' for human-readable timestampsprocess.stdout.isTTYautoLogging.ignore for route skipping (replaces morgan's skip)logger optioncustomLogLevel for status-code-based level selectionreq.log provides child logger with request contextdebug module but doesn't provide general namespace-level controllevel option per-instance (set at creation time)@growi/logger package as a custom wrapper around pino, replacing universal-bunyan>=5.14.0 <10 — pino v10 may not be supported yetlogger.child() usage anywhere in the codebaselogger.fields access or other bunyan-specific APIsloggerFactory(name) — single entry point| Option | Description | Strengths | Risks / Limitations | Notes |
|---|---|---|---|---|
Drop-in wrapper (@growi/logger) |
Shared package providing loggerFactory() over pino with namespace/config/env support |
Minimal call-site changes, single source of truth, testable in isolation | Must implement namespace matching (minimatch) | Mirrors universal-bunyan's role |
| Direct pino usage per app | Each app creates pino instances directly | No wrapper overhead | Duplicated config logic, inconsistent behavior across apps | Rejected: violates Req 8 |
| pino-debug bridge | Use pino-debug for namespace control | Leverages existing package | Only works with debug() calls, not general logging |
Rejected: wrong abstraction |
@growi/logger as Shared Package@growi/logger — clean, purpose-built@growi/logger package in packages/logger/<10autoLogging.ignore for /_next/static/ pathscustomSuccessMessage: (req: IM, res: SR, responseTime: number) => string — called on successful response (statusCode < 500)customErrorMessage: (req: IM, res: SR, error: Error) => string — called on error responsecustomReceivedMessage: (req: IM, res: SR) => string — called when request received (optional, only if autoLogging enabled)customLogLevel: (req: IM, res: SR, error?: Error) => LevelWithSilent — dynamic log level based on status codecustomSuccessObject: (req, res, val) => any — custom fields for successful response logcustomErrorObject: (req, res, error, val) => any — custom fields for error response logcustomAttributeKeys: { req?, res?, err?, reqId?, responseTime? } — rename default keysDate.now() - res[startTime] in millisecondsres.err set, or res.statusCode >= 500customSuccessMessage + customErrorMessage + customLogLevel are sufficient to achieve morgan-like output formatsingleLine: true forces all log properties onto a single linesingleLine: false (default) outputs properties on separate indented linesignore: 'pid,hostname', singleLine produces concise outputmessageFormat option can further customize the format stringsingleLine from false to true in the production FORMAT_NODE_LOG path directly addresses the user's readability concernisFormattedOutputEnabled() returns true when env var is unset; production JSON depends on .env.production.env.production sets FORMAT_NODE_LOG=false — this is the mechanism that ensures JSON in productionFORMAT_NODE_LOG=true explicitly — not affected by default change.env.production fails to load in a Docker override scenario, production would silently get pino-pretty.env.production always loaded by Next.js dotenv-flow).pino-http was initially imported at the module top-level in http-logger.ts. This caused Turbopack to include the Node.js-only module in browser bundles, producing TypeError: __turbopack_context__.r(...).symbols is undefined.@growi/logger is imported by shared page code that runs in both browser and server contexts. Any top-level import of a Node.js-only module (like pino-http) gets pulled into the browser bundle.pino-http import inside the async function body using dynamic import: const { default: pinoHttp } = await import('pino-http'). This defers the import to runtime when the function is actually called (server-side only).@growi/logger.src/dev/)bunyan-format.ts (custom pino transport) and morgan-like-format-options.ts were initially placed at src/transports/ and src/ root respectively, mixed with production modules.src/dev/ directory as the explicit boundary for development-only modules. TransportFactory references ./dev/bunyan-format.js only in the dev branch — the path is never constructed in production code paths.preserveModules: true ensures src/dev/bunyan-format.ts builds to dist/dev/bunyan-format.js with the exact path that pino.transport({ target: ... }) references at runtime.pino.transport() inside loggerFactory(name), spawning a new Worker thread for each namespace.pino.transport() is called once in initializeLoggerFactory, and loggerFactory(name) calls rootLogger.child({ name }) to create namespace-bound loggers sharing the single Worker thread.'trace' (not 'info') so child loggers can independently set their resolved level without being silenced by the root. If the root is 'info', a child with level: 'debug' will still be filtered at the root level.pino.transport() or pino() inside loggerFactory(). All transport setup belongs in initializeLoggerFactory().loggerFactory() returned pino.Logger<never> (the default), which is not assignable to pino-http's expected Logger type.Logger<string> from @growi/logger and type loggerFactory to return Logger<string>. This is compatible with pino-http's logger option.<string> not <never>: pino's default generic CustomLevels is never, which makes the type incompatible with APIs expecting custom levels to potentially be strings. Logger<string> is the correct type for external APIs.@growi/logger Package Visibility"private": true is correct and intentional.apps/app, apps/slackbot-proxy, packages/slack, etc.) are monorepo-internal packages that reference @growi/logger via workspace:* protocol. The private flag only prevents npm publish, not workspace usage. @growi/logger is logging infrastructure — there is no reason to expose it externally (unlike @growi/core or @growi/pluginkit which are published for external plugin developers).