Purpose: @growi/logger is the shared logging infrastructure for the GROWI monorepo, providing namespace-based level control, platform detection (Node.js/browser), and Express HTTP middleware — built on pino.
Users: All GROWI developers (logger consumers), operators (log level configuration), and the CI/CD pipeline.
Scope: All GROWI applications (apps/app, apps/slackbot-proxy) and packages (packages/slack, packages/remark-attachment-refs, packages/remark-lsx) import from @growi/logger as the single logging entry point. Consumer applications do not import pino or pino-http directly.
createHttpLoggerMiddleware() (pino-http encapsulated)@growi/logger entry point for all monorepo consumersgrowi:service:page)@growi/logger to npm (private package, monorepo-internal only)@opentelemetry/instrumentation-pino v10 support)@growi/logger is organized into these layers:
initializeLoggerFactory spawns one Worker thread; loggerFactory(name) returns rootLogger.child({ name }) with resolved levelcreateHttpLoggerMiddleware(); dev-mode morgan-like formatting dynamically imported from src/dev/Key invariants:
loggerFactory(name: string): Logger<string> as the sole logger creation APIpino.transport() called once in initializeLoggerFactory; all namespace loggers share the Worker threadsrc/dev/) are never statically imported in production pathsgraph TB
subgraph ConsumerApps[Consumer Applications]
App[apps/app]
Slackbot[apps/slackbot-proxy]
end
subgraph ConsumerPkgs[Consumer Packages]
Slack[packages/slack]
Remark[packages/remark-attachment-refs]
end
subgraph GrowiLogger[@growi/logger]
Factory[LoggerFactory]
LevelResolver[LevelResolver]
EnvParser[EnvVarParser]
TransportSetup[TransportFactory]
HttpLogger[HttpLoggerFactory]
end
subgraph External[External Packages]
Pino[pino v9.x]
PinoPretty[pino-pretty]
PinoHttp[pino-http]
Minimatch[minimatch]
end
App --> Factory
App --> HttpLogger
Slackbot --> Factory
Slackbot --> HttpLogger
Slack --> Factory
Remark --> Factory
Factory --> LevelResolver
Factory --> TransportSetup
LevelResolver --> EnvParser
LevelResolver --> Minimatch
Factory --> Pino
TransportSetup --> PinoPretty
HttpLogger --> Factory
HttpLogger --> PinoHttp
Architecture Integration:
@growi/logger wraps pino with namespace-level control, transport setup, and HTTP middleware — the single logging entry point for all monorepo consumers@growi/logger owns all logger creation, level resolution, and transport setup; consumer apps only call loggerFactory(name)LevelResolver (namespace-to-level matching), TransportFactory (dev/prod stream setup), EnvVarParser (env variable parsing)packages/ follows monorepo conventionsbunyan-format, morgan-like-format-options) reside under src/dev/ to make the boundary explicit; all are loaded via dynamic import, never statically bundled in production| Layer | Choice / Version | Role in Feature | Notes |
|---|---|---|---|
| Logging Core | pino v9.x | Structured JSON logger for Node.js and browser | Pinned to v9.x for OTel compatibility; see research.md |
| Dev Formatting | pino-pretty v13.x | Human-readable log output in development | Used as transport (worker thread) |
| HTTP Logging | pino-http v11.x | Express middleware for request/response logging | Dependency of @growi/logger; not directly imported by consumer apps |
| Glob Matching | minimatch (existing) | Namespace pattern matching for level config | Already a transitive dependency via universal-bunyan |
| Shared Package | @growi/logger | Logger factory with namespace/config/env support and HTTP middleware | New package in packages/logger/ |
sequenceDiagram
participant App as Application Startup
participant Factory as LoggerFactory
participant Transport as pino.transport (Worker)
participant Root as Root pino Logger
App->>Factory: initializeLoggerFactory(options)
Factory->>Transport: pino.transport(config) — spawns ONE Worker thread
Transport-->>Factory: transport stream
Factory->>Root: pino({ level: 'trace' }, transport)
Root-->>Factory: rootLogger stored in module scope
sequenceDiagram
participant Consumer as Consumer Module
participant Factory as LoggerFactory
participant Cache as Logger Cache
participant Resolver as LevelResolver
participant Root as Root pino Logger
Consumer->>Factory: loggerFactory(namespace)
Factory->>Cache: lookup(namespace)
alt Cache hit
Cache-->>Factory: cached child logger
else Cache miss
Factory->>Resolver: resolveLevel(namespace, config, envOverrides)
Resolver-->>Factory: resolved level
Factory->>Root: rootLogger.child({ name: namespace })
Root-->>Factory: child logger (shares Worker thread)
Factory->>Factory: childLogger.level = resolved level
Factory->>Cache: store(namespace, childLogger)
end
Factory-->>Consumer: Logger
flowchart TD
Start[resolveLevel namespace] --> EnvCheck{Env var match?}
EnvCheck -->|Yes| EnvLevel[Use env var level]
EnvCheck -->|No| ConfigCheck{Config pattern match?}
ConfigCheck -->|Yes| ConfigLevel[Use config level]
ConfigCheck -->|No| DefaultLevel[Use config default level]
EnvLevel --> Done[Return level]
ConfigLevel --> Done
DefaultLevel --> Done
| Requirement | Summary | Components | Interfaces | Flows |
|---|---|---|---|---|
| 1.1–1.4 | Logger factory with namespace support | LoggerFactory, LoggerCache | loggerFactory() |
Logger Creation |
| 2.1–2.4 | Config-file level control | LevelResolver, ConfigLoader | LoggerConfig type |
Level Resolution |
| 3.1–3.5 | Env var level override | EnvVarParser, LevelResolver | parseEnvLevels() |
Level Resolution |
| 4.1–4.4 | Platform-aware logger | LoggerFactory, TransportFactory | createTransport() |
Logger Creation |
| 5.1–5.4 | Dev/prod output formatting | TransportFactory | TransportOptions |
Logger Creation |
| 6.1–6.4 | HTTP request logging | HttpLoggerMiddleware | createHttpLogger() |
— |
| 7.1–7.3 | OpenTelemetry integration | DiagLoggerPinoAdapter | DiagLogger interface |
— |
| 8.1–8.5 | Multi-app consistency | @growi/logger package | Package exports | — |
| 10.1–10.3 | Pino logger type export | LoggerFactory | Logger<string> export |
— |
| 11.1–11.4 | Pino performance preservation | LoggerFactory | initializeLoggerFactory, shared root logger |
Logger Creation |
| 12.1–12.6 | Bunyan-like output format | BunyanFormatTransport, TransportFactory | Custom transport target | Logger Creation |
| 13.1–13.5 | HTTP logger encapsulation | HttpLoggerFactory | createHttpLoggerMiddleware() |
— |
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies | Contracts |
|---|---|---|---|---|---|
| LoggerFactory | @growi/logger / Core | Create and cache namespace-bound pino loggers | 1, 4, 8, 10, 11 | pino (P0), LevelResolver (P0), TransportFactory (P0) | Service |
| LevelResolver | @growi/logger / Core | Resolve log level for a namespace from config + env | 2, 3 | minimatch (P0), EnvVarParser (P0) | Service |
| EnvVarParser | @growi/logger / Core | Parse env vars into namespace-level map | 3 | — | Service |
| TransportFactory | @growi/logger / Core | Create pino transport/options for Node.js and browser | 4, 5, 12 | pino-pretty (P1) | Service |
| BunyanFormatTransport | @growi/logger / Transport | Custom pino transport producing bunyan-format "short" output | 12 | pino-pretty (P1) | Transport |
| HttpLoggerFactory | @growi/logger / Core | Factory for pino-http Express middleware | 6, 13 | pino-http (P0), LoggerFactory (P0) | Service |
| DiagLoggerPinoAdapter | apps/app / OpenTelemetry | Wrap pino logger as OTel DiagLogger | 7 | pino (P0) | Service |
| ConfigLoader | Per-app | Load dev/prod config files | 2 | — | — |
| Field | Detail |
|---|---|
| Intent | Central entry point for creating namespace-bound pino loggers with level resolution and caching |
| Requirements | 1.1, 1.2, 1.3, 1.4, 4.1, 8.5, 10.1, 10.3 |
Responsibilities & Constraints
loggerFactory(name: string): pino.Logger as the public APIDependencies
Contracts: Service [x]
import type { Logger } from 'pino';
interface LoggerConfig {
[namespacePattern: string]: string; // pattern → level ('info', 'debug', etc.)
}
interface LoggerFactoryOptions {
config: LoggerConfig;
}
/**
* Initialize the logger factory module with configuration.
* Must be called once at application startup before any loggerFactory() calls.
*/
function initializeLoggerFactory(options: LoggerFactoryOptions): void;
/**
* Create or retrieve a cached pino logger for the given namespace.
*/
function loggerFactory(name: string): Logger;
initializeLoggerFactory() called before first loggerFactory() callImplementation Notes
initializeLoggerFactory is called once per app at startup, receiving the merged dev/prod configtypeof window !== 'undefined' && typeof window.document !== 'undefined'browser optionpino.transport() spawns a Worker thread. It MUST be called once inside initializeLoggerFactory, not inside loggerFactory. Each loggerFactory(name) call creates a child logger via rootLogger.child({ name }) which shares the single Worker thread. Calling pino.transport() per namespace would spawn N Worker threads for N namespaces, negating pino's core performance advantage.| Field | Detail |
|---|---|
| Intent | Determine the effective log level for a given namespace by matching against config patterns and env var overrides |
| Requirements | 2.1, 2.3, 2.4, 3.1, 3.2, 3.3, 3.4, 3.5 |
Responsibilities & Constraints
default level as fallbackDependencies
Contracts: Service [x]
interface LevelResolver {
/**
* Resolve the log level for a namespace.
* Priority: env var match > config pattern match > config default.
*/
resolveLevel(
namespace: string,
config: LoggerConfig,
envOverrides: LoggerConfig,
): string;
}
config contains a default key| Field | Detail |
|---|---|
| Intent | Parse environment variables (DEBUG, TRACE, INFO, WARN, ERROR, FATAL) into a namespace-to-level map |
| Requirements | 3.1, 3.4, 3.5 |
Responsibilities & Constraints
process.env.DEBUG, process.env.TRACE, etc.LoggerConfig map: { 'growi:*': 'debug', 'growi:service:page': 'trace' }Contracts: Service [x]
/**
* Parse log-level environment variables into a namespace-to-level map.
* Reads: DEBUG, TRACE, INFO, WARN, ERROR, FATAL from process.env.
*/
function parseEnvLevels(): LoggerConfig;
process.env)| Field | Detail |
|---|---|
| Intent | Create pino transport configuration appropriate for the current environment |
| Requirements | 4.1, 4.2, 4.3, 4.4, 5.1, 5.2, 5.3, 5.4, 12.1, 12.6, 12.7, 12.8 |
Responsibilities & Constraints
singleLine: false) — dev only, not imported in productionFORMAT_NODE_LOG: return standard pino-pretty transport with singleLine: true (not bunyan-format)browser option config (console output, production error-level default)name field in all output via pino's name optionContracts: Service [x]
import type { LoggerOptions } from 'pino';
interface TransportConfig {
/** Pino options for Node.js environment */
nodeOptions: Partial<LoggerOptions>;
/** Pino options for browser environment */
browserOptions: Partial<LoggerOptions>;
}
/**
* Create transport configuration based on environment.
* @param isProduction - Whether NODE_ENV is 'production'
*/
function createTransportConfig(isProduction: boolean): TransportConfig;
Implementation Notes
{ target: '<resolved-path>/dev/bunyan-format.js' } — target path resolved via path.join(path.dirname(fileURLToPath(import.meta.url)), 'dev', 'bunyan-format.js'); no options passed (singleLine defaults to false inside the module){ target: 'pino-pretty', options: { translateTime: 'SYS:standard', ignore: 'pid,hostname', singleLine: true } } — standard pino-pretty, no custom prettifiers{ browser: { asObject: false }, level: 'error' }{ browser: { asObject: false } } (inherits resolved level)| Field | Detail |
|---|---|
| Intent | Custom pino transport that produces bunyan-format "short" mode output (development only) |
| Requirements | 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7 |
Responsibilities & Constraints
pino.transport() in a Worker thread — must be a module file, not inline functionscustomPrettifiers to match bunyan-format "short" layoutDependencies
Contracts: Transport [x]
// packages/logger/src/dev/bunyan-format.ts
// Default export: function(opts) → Writable stream (pino transport protocol)
interface BunyanFormatOptions {
singleLine?: boolean;
colorize?: boolean;
destination?: NodeJS.WritableStream;
}
Implementation Notes
messageFormat in pino-pretty to produce the full line: timestamp + level + name + messageignore: 'pid,hostname,name,req,res,responseTime' — suppresses pino-http's verbose req/res objects in dev; the morgan-like customSuccessMessage already provides method/URL/status/time on the same linecustomPrettifiers: { time: () => '', level: () => '' } — suppresses pino-pretty's default time/level rendering (handled inside messageFormat)messageFormat using ANSI codessingleLine defaults to false inside the module; no options need to be passed from TransportFactorypreserveModules ensures src/dev/bunyan-format.ts → dist/dev/bunyan-format.jsNO_COLOR environment variable is respected to disable colorizationDev (bunyan-format, singleLine: false):
10:06:30.419Z DEBUG growi:service:PassportService: LdapStrategy: serverUrl is invalid
10:06:30.420Z WARN growi:service:PassportService: SamlStrategy: cert is not set.
extra: {"field":"value"}
Dev HTTP log (bunyan-format + morgan-like format, req/res suppressed):
10:06:30.730Z INFO express: GET /applicable-grant?pageId=abc 304 - 16ms
Prod + FORMAT_NODE_LOG (standard pino-pretty, singleLine: true):
[2026-03-30 12:00:00.000] INFO (growi:service:search): Elasticsearch is enabled
Prod default: raw JSON (no transport, unchanged)
| Field | Detail |
|---|---|
| Intent | Encapsulate pino-http middleware creation within @growi/logger so consumers don't depend on pino-http |
| Requirements | 6.1, 6.2, 6.3, 6.4, 13.1, 13.2, 13.3, 13.4, 13.5, 13.6 |
Responsibilities & Constraints
morganLikeFormatOptions (customSuccessMessage, customErrorMessage, customLogLevel)autoLogging configuration for route filteringpino-http as an internal dependency of @growi/loggerDependencies
Contracts: Service [x]
import type { RequestHandler } from 'express';
interface HttpLoggerOptions {
/** Logger namespace, defaults to 'express' */
namespace?: string;
/** Auto-logging configuration (e.g., route ignore patterns) */
autoLogging?: {
ignore: (req: { url?: string }) => boolean;
};
}
/**
* Create Express middleware for HTTP request logging.
* In dev: uses pino-http with morgan-like formatting (dynamically imported).
* In prod: uses pino-http with default formatting.
*/
async function createHttpLoggerMiddleware(options?: HttpLoggerOptions): Promise<RequestHandler>;
Implementation Notes
pino-http moves from apps' dependencies to @growi/logger's dependenciespino-http is imported lazily inside the function body (const { default: pinoHttp } = await import('pino-http')) rather than at the module top-level. This prevents bundlers (Turbopack/webpack) from pulling the Node.js-only pino-http into browser bundles when @growi/logger is imported by shared codemorganLikeFormatOptions is dynamically imported (await import('./dev/morgan-like-format-options')) only when NODE_ENV !== 'production', ensuring the module is not loaded in productionasync to support the dynamic imports; consumers call: express.use(await createHttpLoggerMiddleware({ autoLogging: { ignore: ... } }))| Field | Detail |
|---|---|
| Intent | Adapt a pino logger to the OpenTelemetry DiagLogger interface |
| Requirements | 7.1, 7.2, 7.3 |
Responsibilities & Constraints
DiagLogger interface (error, warn, info, debug, verbose)verbose() to pino's trace() level@opentelemetry/instrumentation-pino if enabled by defaultDependencies
Contracts: Service [x]
import type { DiagLogger } from '@opentelemetry/api';
/**
* Create a DiagLogger that delegates to a pino logger.
* Maps OTel verbose level to pino trace level.
*/
function createDiagLoggerAdapter(): DiagLogger;
Implementation Notes
DiagLoggerBunyanAdapter — rename class, update import from bunyan to pinoparseMessage helper can remain largely unchanged'@opentelemetry/instrumentation-bunyan': { enabled: false } with '@opentelemetry/instrumentation-pino': { enabled: false } if the instrumentation package is presentNot applicable. This feature modifies runtime logging behavior and does not introduce or change persisted data models.
Logging infrastructure must be resilient — a logger failure must never crash the application.
{ default: 'info' } and emit a console warningprocess.stderr directly (cannot use the logger itself)