Просмотр исходного кода

Update design and requirements for logger migration; implement bunyan-like output format and encapsulate HTTP logger middleware

Yuki Takei 5 дней назад
Родитель
Сommit
4dc3dd50a2

+ 122 - 30
.kiro/specs/migrate-logger-to-pino/design.md

@@ -6,7 +6,7 @@
 
 
 **Users**: All GROWI developers (logger consumers), operators (log level configuration), and the CI/CD pipeline (dependency management).
 **Users**: All GROWI developers (logger consumers), operators (log level configuration), and the CI/CD pipeline (dependency management).
 
 
-**Impact**: Replaces 7 logging-related packages (`bunyan`, `universal-bunyan`, `bunyan-format`, `express-bunyan-logger`, `morgan`, `browser-bunyan`, `@browser-bunyan/console-formatted-stream`) with 3 (`pino`, `pino-pretty`, `pino-http`) plus a new shared package `@growi/logger`.
+**Impact**: Replaces 7 logging-related packages (`bunyan`, `universal-bunyan`, `bunyan-format`, `express-bunyan-logger`, `morgan`, `browser-bunyan`, `@browser-bunyan/console-formatted-stream`) with 3 (`pino`, `pino-pretty`, `pino-http`) plus a new shared package `@growi/logger`. Consumer applications import only `@growi/logger`; `pino-http` is encapsulated within the package.
 
 
 ### Goals
 ### Goals
 - Replace bunyan with pino across all apps and packages without functional degradation
 - Replace bunyan with pino across all apps and packages without functional degradation
@@ -98,9 +98,9 @@ graph TB
 |-------|------------------|-----------------|-------|
 |-------|------------------|-----------------|-------|
 | Logging Core | pino v9.x | Structured JSON logger for Node.js and browser | Pinned to v9.x for OTel compatibility; see research.md |
 | 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) |
 | 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 | Replaces both morgan and express-bunyan-logger |
+| 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 |
 | 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 | New package in packages/logger/ |
+| Shared Package | @growi/logger | Logger factory with namespace/config/env support and HTTP middleware | New package in packages/logger/ |
 
 
 ## System Flows
 ## System Flows
 
 
@@ -173,6 +173,8 @@ flowchart TD
 | 9.1–9.3 | Dependency cleanup | — (removal task) | — | — |
 | 9.1–9.3 | Dependency cleanup | — (removal task) | — | — |
 | 10.1–10.3 | Backward-compatible API | LoggerFactory | `Logger` type export | — |
 | 10.1–10.3 | Backward-compatible API | LoggerFactory | `Logger` type export | — |
 | 11.1–11.4 | Pino performance preservation | LoggerFactory | `initializeLoggerFactory`, shared root logger | Logger Creation |
 | 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()` | — |
 
 
 ## Components and Interfaces
 ## Components and Interfaces
 
 
@@ -181,8 +183,9 @@ flowchart TD
 | LoggerFactory | @growi/logger / Core | Create and cache namespace-bound pino loggers | 1, 4, 8, 10, 11 | pino (P0), LevelResolver (P0), TransportFactory (P0) | Service |
 | 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 |
 | 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 |
 | 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 | pino-pretty (P1) | Service |
-| HttpLoggerMiddleware | apps/app, apps/slackbot-proxy | Express middleware for HTTP request logging | 6 | pino-http (P0), LoggerFactory (P0) | 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 |
 | DiagLoggerPinoAdapter | apps/app / OpenTelemetry | Wrap pino logger as OTel DiagLogger | 7 | pino (P0) | Service |
 | ConfigLoader | Per-app | Load dev/prod config files | 2 | — | — |
 | ConfigLoader | Per-app | Load dev/prod config files | 2 | — | — |
 
 
@@ -317,11 +320,12 @@ function parseEnvLevels(): LoggerConfig;
 | Field | Detail |
 | Field | Detail |
 |-------|--------|
 |-------|--------|
 | Intent | Create pino transport configuration appropriate for the current environment |
 | 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 |
+| 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**
 **Responsibilities & Constraints**
-- Node.js development: return pino-pretty transport config (`singleLine`, `ignore: 'pid,hostname'`, `translateTime`)
-- Node.js production: return raw JSON (stdout) by default; formatted output when `FORMAT_NODE_LOG` is truthy
+- Node.js development: return BunyanFormatTransport config (`singleLine: false`) — **dev only, not imported in production**
+- Node.js production + `FORMAT_NODE_LOG`: return standard `pino-pretty` transport with `singleLine: true` (not bunyan-format)
+- Node.js production default: return raw JSON (stdout) — no transport
 - Browser: return pino `browser` option config (console output, production error-level default)
 - Browser: return pino `browser` option config (console output, production error-level default)
 - Include `name` field in all output via pino's `name` option
 - Include `name` field in all output via pino's `name` option
 
 
@@ -351,26 +355,81 @@ function createTransportConfig(isProduction: boolean): TransportConfig;
 - Invariants: Browser options never include Node.js transports
 - Invariants: Browser options never include Node.js transports
 
 
 **Implementation Notes**
 **Implementation Notes**
-- Dev transport: `{ target: 'pino-pretty', options: { translateTime: 'SYS:standard', ignore: 'pid,hostname', singleLine: false } }`
-- Prod with FORMAT_NODE_LOG: use pino-pretty transport with long format
+- Dev transport: `{ target: '<resolved-path>/transports/bunyan-format.js', options: { singleLine: false } }` — target path resolved via `import.meta.url`
+- Prod with FORMAT_NODE_LOG: `{ target: 'pino-pretty', options: { translateTime: 'SYS:standard', ignore: 'pid,hostname', singleLine: true } }` — standard pino-pretty, no custom prettifiers
 - Prod without FORMAT_NODE_LOG (or false): raw JSON to stdout (no transport)
 - Prod without FORMAT_NODE_LOG (or false): raw JSON to stdout (no transport)
 - Browser production: `{ browser: { asObject: false }, level: 'error' }`
 - Browser production: `{ browser: { asObject: false }, level: 'error' }`
 - Browser development: `{ browser: { asObject: false } }` (inherits resolved level)
 - Browser development: `{ browser: { asObject: false } }` (inherits resolved level)
+- **Important**: The bunyan-format transport path is only resolved/referenced in the dev branch, ensuring the module is never imported in production
+
+#### BunyanFormatTransport
+
+| 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**
+- Loaded by `pino.transport()` in a Worker thread — must be a module file, not inline functions
+- Uses pino-pretty internally with `customPrettifiers` to match bunyan-format "short" layout
+- **Development only**: This module is only referenced by TransportFactory in the dev branch; never imported in production
+
+**Dependencies**
+- External: pino-pretty v13.x (P1) — used internally for colorization and base formatting
+
+**Contracts**: Transport [x]
+
+##### Transport Module
+
+```typescript
+// packages/logger/src/transports/bunyan-format.ts
+// Default export: function(opts) → Writable stream (pino transport protocol)
+
+interface BunyanFormatOptions {
+  singleLine?: boolean;
+}
+```
+
+**Implementation Notes**
+- Internally calls `pinoPretty({ ...opts, ignore: 'pid,hostname,name', customPrettifiers: { time, level } })`
+- `customPrettifiers.time`: formats epoch → `HH:mm:ss.SSSZ` (UTC time-only, no brackets)
+- `customPrettifiers.level`: returns `${label.padStart(5)} ${log.name}` (right-aligned level + namespace, no parens)
+- pino-pretty then appends `: ` and the message, producing: `10:06:30.419Z DEBUG growi:service:page: message`
+- Since the transport is a separate module loaded by the Worker thread, function options work (no serialization issue)
+- Vite's `preserveModules` ensures `src/transports/bunyan-format.ts` → `dist/transports/bunyan-format.js`
+
+##### Output Examples
+
+**Dev** (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"}
+```
+
+**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)
 
 
 ### HTTP Logging Layer
 ### HTTP Logging Layer
 
 
-#### HttpLoggerMiddleware
+#### HttpLoggerFactory
 
 
 | Field | Detail |
 | Field | Detail |
 |-------|--------|
 |-------|--------|
-| Intent | Provide Express middleware for HTTP request/response logging via pino-http |
-| Requirements | 6.1, 6.2, 6.3, 6.4 |
+| 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**
 **Responsibilities & Constraints**
 - Create pino-http middleware using a logger from LoggerFactory
 - Create pino-http middleware using a logger from LoggerFactory
-- Skip `/_next/static/` paths in development
-- Include method, URL, status code, and response time in log entries
-- Unified middleware for both dev and prod (replaces morgan + express-bunyan-logger)
+- In development mode: dynamically import and apply `morganLikeFormatOptions` (customSuccessMessage, customErrorMessage, customLogLevel)
+- In production mode: use pino-http's default message format (no morgan-like module imported)
+- Accept optional `autoLogging` configuration for route filtering
+- Return Express-compatible middleware
+- Encapsulate `pino-http` as an internal dependency of `@growi/logger`
 
 
 **Dependencies**
 **Dependencies**
 - External: pino-http v11.x (P0)
 - External: pino-http v11.x (P0)
@@ -383,21 +442,32 @@ function createTransportConfig(isProduction: boolean): TransportConfig;
 ```typescript
 ```typescript
 import type { RequestHandler } from 'express';
 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.
  * Create Express middleware for HTTP request logging.
- * Uses pino-http with the 'express' namespace logger.
+ * In dev: uses pino-http with morgan-like formatting (dynamically imported).
+ * In prod: uses pino-http with default formatting.
  */
  */
-function createHttpLoggerMiddleware(): RequestHandler;
+async function createHttpLoggerMiddleware(options?: HttpLoggerOptions): Promise<RequestHandler>;
 ```
 ```
 
 
 - Preconditions: LoggerFactory initialized
 - Preconditions: LoggerFactory initialized
 - Postconditions: Returns Express middleware that logs HTTP requests
 - Postconditions: Returns Express middleware that logs HTTP requests
-- Invariants: Static file paths are skipped in non-production mode
+- Invariants: morganLikeFormatOptions applied only in dev; static file paths skipped when autoLogging.ignore provided
 
 
 **Implementation Notes**
 **Implementation Notes**
-- Use `autoLogging.ignore` for route filtering: `(req) => req.url?.startsWith('/_next/static/')`
-- In production, do not skip any routes (current express-bunyan-logger uses `excludes: ['*']` which excludes fields, not routes; pino-http's `customAttributeKeys` can control which fields are included)
-- `quietReqLogger: true` for lighter child loggers on `req.log`
+- The type assertion for Logger<string> → pino-http's Logger is handled internally, hidden from consumers
+- `pino-http` moves from apps' dependencies to `@growi/logger`'s dependencies
+- `morganLikeFormatOptions` is dynamically imported (`await import('./morgan-like-format-options')`) only when `NODE_ENV !== 'production'`, ensuring the module is not loaded in production
+- The function is `async` to support the dynamic import; consumers call: `express.use(await createHttpLoggerMiddleware({ autoLogging: { ignore: ... } }))`
 
 
 ### OpenTelemetry Layer
 ### OpenTelemetry Layer
 
 
@@ -557,26 +627,48 @@ const customLogLevel: PinoHttpOptions['customLogLevel'] = (_req, res, error) =>
 };
 };
 ```
 ```
 
 
-### Output Examples
+### Output Examples (Updated with dev-only bunyan-like format)
 
 
-**Dev** (pino-pretty, singleLine: false):
+**Dev** (bunyan-format transport + morgan-like HTTP messages):
 ```
 ```
-[2026-03-30 12:00:00.000] INFO (express): GET /page/path 200 - 12ms
+10:06:30.419Z  INFO express: GET /page/path 200 - 12ms
     req: {"method":"GET","url":"/page/path"}
     req: {"method":"GET","url":"/page/path"}
     res: {"statusCode":200}
     res: {"statusCode":200}
 ```
 ```
 
 
-**Prod + FORMAT_NODE_LOG=true** (pino-pretty, singleLine: true):
+**Prod + FORMAT_NODE_LOG=true** (standard pino-pretty, default pino-http messages):
 ```
 ```
-[2026-03-30 12:00:00.000] INFO (express): GET /page/path 200 - 12ms
+[2026-03-30 12:00:00.000] INFO (express): request completed
 ```
 ```
 
 
-**Prod default** (JSON):
+**Prod default** (JSON, default pino-http messages):
 ```json
 ```json
-{"level":30,"time":1711792800000,"name":"express","msg":"GET /page/path 200 - 12ms","req":{"method":"GET","url":"/page/path"},"res":{"statusCode":200},"responseTime":12}
+{"level":30,"time":1711792800000,"name":"express","msg":"request completed","req":{"method":"GET","url":"/page/path"},"res":{"statusCode":200},"responseTime":12}
 ```
 ```
 
 
 ### Testing
 ### Testing
 
 
-- `transport-factory.spec.ts`: Verify prod + FORMAT_NODE_LOG returns `singleLine: true`; dev returns `singleLine: false`
+- `transport-factory.spec.ts`: Verify transport target is bunyan-format (not pino-pretty directly); dev returns `singleLine: false`, prod + FORMAT_NODE_LOG returns `singleLine: true`
+- `bunyan-format.spec.ts` (optional): Verify the transport module is loadable and produces expected output format
+- `http-logger.spec.ts`: Verify `createHttpLoggerMiddleware` returns middleware, applies morganLikeFormatOptions, passes autoLogging options
 - pino-http custom messages: Verify `customSuccessMessage` format, `customLogLevel` returns correct levels for 2xx/4xx/5xx
 - pino-http custom messages: Verify `customSuccessMessage` format, `customLogLevel` returns correct levels for 2xx/4xx/5xx
+
+---
+
+## Addendum: HTTP Logger Encapsulation (Post-Migration)
+
+> Added 2026-04-02. Moves pino-http usage from consumer apps into @growi/logger.
+
+### Background
+
+- Consumer apps (`apps/app`, `apps/slackbot-proxy`) currently import `pino-http` directly
+- This leaks implementation details and requires each app to configure morgan-like format options
+- Encapsulating in `@growi/logger` provides a single configuration point and cleaner dependency graph
+
+### Changes
+
+1. **New file**: `packages/logger/src/http-logger.ts` — exports `createHttpLoggerMiddleware(options)`
+2. **Package.json**: Add `pino-http` to `@growi/logger` dependencies
+3. **apps/app**: Replace direct `pino-http` import with `createHttpLoggerMiddleware` from `@growi/logger`
+4. **apps/slackbot-proxy**: Same as apps/app
+5. **Cleanup**: Remove `pino-http` from apps' direct dependencies (keep in @growi/logger)

+ 26 - 0
.kiro/specs/migrate-logger-to-pino/requirements.md

@@ -125,3 +125,29 @@ This specification covers the complete migration from bunyan to pino, replacing
 1. The pino logger shall support `.info()`, `.debug()`, `.warn()`, `.error()`, `.trace()`, and `.fatal()` methods with the same argument patterns as bunyan (message string, optional object, optional error).
 1. The pino logger shall support `.info()`, `.debug()`, `.warn()`, `.error()`, `.trace()`, and `.fatal()` methods with the same argument patterns as bunyan (message string, optional object, optional error).
 2. If bunyan-specific APIs (e.g., `logger.child()`, serializers) are used at any call sites, the pino equivalent shall be provided or the call site shall be adapted.
 2. If bunyan-specific APIs (e.g., `logger.child()`, serializers) are used at any call sites, the pino equivalent shall be provided or the call site shall be adapted.
 3. The Logger Factory shall export a TypeScript type for the logger instance that is compatible with the pino Logger type.
 3. The Logger Factory shall export a TypeScript type for the logger instance that is compatible with the pino Logger type.
+
+### Requirement 12: Bunyan-Like Output Format (Development Only)
+
+**Objective:** As a developer, I want the log output in development mode to resemble bunyan-format's "short" mode, so that the visual experience remains familiar after migration.
+
+#### Acceptance Criteria
+1. While in development mode (`NODE_ENV !== 'production'`), the Logger Factory shall output each log line in the format: `HH:mm:ss.SSSZ LEVEL name: message` (e.g., `10:06:30.419Z DEBUG growi:service:page: some message`).
+2. The level label shall be right-aligned to 5 characters (e.g., `DEBUG`, ` INFO`, ` WARN`).
+3. The timestamp shall be UTC time-only in ISO 8601 format (`HH:mm:ss.SSSZ`), without date or surrounding brackets.
+4. The logger namespace (`name` field) shall appear directly after the level label, followed by a colon and the message, without parentheses.
+5. Log lines shall be colorized by level (cyan for DEBUG, green for INFO, yellow for WARN, red for ERROR).
+6. The bunyan-like format shall be implemented as a custom pino transport module within `@growi/logger`, so that `pino.transport()` can load it in a worker thread without function serialization issues.
+7. The bunyan-format transport module shall only be imported in development mode. In production, the module shall not be imported or bundled.
+8. While in production mode with `FORMAT_NODE_LOG` enabled, the Logger Factory shall use standard pino-pretty (not the bunyan-format transport) for formatted output.
+
+### Requirement 13: HTTP Logger Middleware Encapsulation
+
+**Objective:** As a developer, I want the HTTP request logging middleware encapsulated within `@growi/logger`, so that consumer applications do not need to depend on or import `pino-http` directly.
+
+#### Acceptance Criteria
+1. The `@growi/logger` package shall export a `createHttpLoggerMiddleware(options)` function that returns Express-compatible middleware for HTTP request logging.
+2. The middleware factory shall accept options for the logger namespace (defaulting to `'express'`) and optional `autoLogging` configuration (e.g., route ignore patterns).
+3. While in development mode, the middleware shall apply morgan-like formatting (custom success/error messages, custom log levels) via dynamic import. In production mode, the morgan-like format module shall not be imported; pino-http's default message format shall be used.
+4. After the encapsulation, `apps/app` and `apps/slackbot-proxy` shall not import `pino-http` directly; all HTTP logging shall go through `@growi/logger`.
+5. The `pino-http` dependency shall move from consumer applications to `@growi/logger`'s `dependencies`.
+6. The `morganLikeFormatOptions` module shall only be imported in development mode (dynamic import). In production, the module shall not be imported or bundled.

+ 1 - 1
.kiro/specs/migrate-logger-to-pino/spec.json

@@ -1,7 +1,7 @@
 {
 {
   "feature_name": "migrate-logger-to-pino",
   "feature_name": "migrate-logger-to-pino",
   "created_at": "2026-03-23T00:00:00.000Z",
   "created_at": "2026-03-23T00:00:00.000Z",
-  "updated_at": "2026-03-30T00:00:00.000Z",
+  "updated_at": "2026-04-02T00:00:00.000Z",
   "language": "en",
   "language": "en",
   "phase": "tasks-generated",
   "phase": "tasks-generated",
   "approvals": {
   "approvals": {

+ 64 - 0
.kiro/specs/migrate-logger-to-pino/tasks.md

@@ -197,3 +197,67 @@
   - Run lint and type-check for apps/app and apps/slackbot-proxy
   - Run lint and type-check for apps/app and apps/slackbot-proxy
   - Verify the production build succeeds
   - Verify the production build succeeds
   - _Requirements: 5.1, 5.3, 6.1, 6.4_
   - _Requirements: 5.1, 5.3, 6.1, 6.4_
+
+- [ ] 12. Implement bunyan-like output format (development only)
+- [ ] 12.1 Create the bunyan-format custom transport module
+  - Create `packages/logger/src/transports/bunyan-format.ts` that default-exports a function returning a pino-pretty stream
+  - Use `customPrettifiers.time` to format epoch as `HH:mm:ss.SSSZ` (UTC time-only, no brackets)
+  - Use `customPrettifiers.level` to return `${label.padStart(5)} ${log.name}` (right-aligned 5-char level + namespace)
+  - Set `ignore: 'pid,hostname,name'` so name appears via the level prettifier, not in pino-pretty's default parens
+  - Accept `singleLine` option to pass through to pino-pretty
+  - Verify the module is built to `dist/transports/bunyan-format.js` by vite's `preserveModules` config
+  - _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6_
+
+- [ ] 12.2 Update TransportFactory to use bunyan-format transport in dev only
+  - In the **development** branch of `createNodeTransportOptions`, change the transport target from `'pino-pretty'` to the resolved path of `bunyan-format.js` (via `import.meta.url`)
+  - Remove `translateTime` and `ignore` options from the dev transport config (now handled inside the custom transport)
+  - Pass `singleLine: false` for dev
+  - In the **production + FORMAT_NODE_LOG** branch, keep `target: 'pino-pretty'` with standard options (`translateTime: 'SYS:standard'`, `ignore: 'pid,hostname'`, `singleLine: true`) — do NOT use bunyan-format
+  - The bunyan-format module path is only resolved in the dev code path, ensuring it is never imported in production
+  - Update unit tests in `transport-factory.spec.ts`: dev target contains `bunyan-format`; prod + FORMAT_NODE_LOG target is `'pino-pretty'`
+  - _Requirements: 12.1, 12.6, 12.7, 12.8_
+
+- [ ] 12.3 Verify bunyan-format output
+  - Run the dev server and confirm log output matches the bunyan-format "short" style: `HH:mm:ss.SSSZ LEVEL name: message`
+  - Confirm colorization works (DEBUG=cyan, INFO=green, WARN=yellow, ERROR=red)
+  - Confirm multi-line output in dev (extra fields on subsequent lines)
+  - _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5_
+
+- [ ] 13. Encapsulate pino-http in @growi/logger
+- [ ] 13.1 Create HTTP logger middleware factory in @growi/logger
+  - Create `packages/logger/src/http-logger.ts` exporting `async createHttpLoggerMiddleware(options?)`
+  - The function creates `pinoHttp` middleware internally with `loggerFactory(namespace)`
+  - In development mode (`NODE_ENV !== 'production'`): dynamically import `morganLikeFormatOptions` via `await import('./morgan-like-format-options')` and apply to pino-http options
+  - In production mode: use pino-http with default message formatting (no morgan-like module imported)
+  - Accept optional `namespace` (default: `'express'`) and `autoLogging` options
+  - Handle the `Logger<string>` → pino-http's expected Logger type assertion internally
+  - Add `pino-http` to `@growi/logger` package.json dependencies
+  - Export `createHttpLoggerMiddleware` from `packages/logger/src/index.ts`
+  - _Requirements: 13.1, 13.2, 13.3, 13.5, 13.6_
+
+- [ ] 13.2 (P) Migrate apps/app to use createHttpLoggerMiddleware
+  - Replace the direct `pinoHttp` import and configuration in `apps/app/src/server/crowi/index.ts` with `await createHttpLoggerMiddleware(...)` from `@growi/logger`
+  - Pass the `/_next/static/` autoLogging ignore function via the options
+  - Remove `pino-http` and its type imports from the file
+  - Remove `morganLikeFormatOptions` import (now applied internally in dev only)
+  - Remove `pino-http` from `apps/app/package.json` if no longer directly used
+  - Run `pnpm --filter @growi/app lint:typecheck` to confirm no type errors
+  - _Requirements: 13.4_
+
+- [ ] 13.3 (P) Migrate apps/slackbot-proxy to use createHttpLoggerMiddleware
+  - Replace the direct `pinoHttp` import and configuration in `apps/slackbot-proxy/src/Server.ts` with `await createHttpLoggerMiddleware(...)` from `@growi/logger`
+  - Remove `pino-http` and its type imports from the file
+  - Remove `morganLikeFormatOptions` import (now applied internally in dev only)
+  - Remove the `as unknown as` type assertion (now handled internally)
+  - Remove `pino-http` from `apps/slackbot-proxy/package.json` if no longer directly used
+  - Run `pnpm --filter @growi/slackbot-proxy lint:typecheck` to confirm no type errors
+  - _Requirements: 13.4_
+
+- [ ] 14. Validate bunyan-format and HTTP encapsulation
+- [ ] 14.1 Run full validation
+  - Run `@growi/logger` package tests
+  - Run lint and type-check for apps/app and apps/slackbot-proxy
+  - Run `turbo run build --filter @growi/app` to verify production build succeeds
+  - Verify no remaining direct `pino-http` imports in apps/app or apps/slackbot-proxy source files
+  - Verify that bunyan-format transport and morganLikeFormatOptions are NOT imported in production (grep for dynamic import pattern)
+  - _Requirements: 12.1, 12.6, 12.7, 13.4, 13.5, 13.6_