Yuki Takei 2 tygodni temu
rodzic
commit
a1ef4c1c43

+ 483 - 0
.kiro/specs/migrate-logger-to-pino/design.md

@@ -0,0 +1,483 @@
+# Design Document: migrate-logger-to-pino
+
+## Overview
+
+**Purpose**: This feature migrates GROWI's logging infrastructure from bunyan (with the custom `universal-bunyan` wrapper) to pino, delivering faster structured logging with a smaller dependency footprint.
+
+**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`.
+
+### Goals
+- Replace bunyan with pino across all apps and packages without functional degradation
+- Preserve namespace-based log level control (config files + env var overrides)
+- Eliminate morgan by consolidating HTTP logging into pino-http
+- Maintain OpenTelemetry diagnostic logger integration
+- Provide a shared `@growi/logger` package as the single logging entry point
+
+### Non-Goals
+- Changing log output semantics (field names, message format) beyond what pino naturally produces
+- Adding new logging capabilities (structured context propagation, remote log shipping)
+- Migrating to pino v10 (deferred until OTel instrumentation supports it)
+- Changing the namespace naming convention (e.g., `growi:service:page`)
+
+## Architecture
+
+### Existing Architecture Analysis
+
+The current logging stack has these layers:
+
+1. **universal-bunyan** — custom wrapper providing: namespace-based level control via config + env vars, platform detection (Node.js/browser), stream selection (bunyan-format for Node.js, ConsoleFormattedStream for browser), logger caching
+2. **Per-app loggerFactory** — thin wrapper that loads dev/prod config and delegates to universal-bunyan
+3. **bunyan / browser-bunyan** — underlying logger implementations
+4. **express-bunyan-logger / morgan** — HTTP request logging middleware
+
+Key patterns to preserve:
+- `loggerFactory(name: string): Logger` as the sole logger creation API
+- Hierarchical colon-delimited namespaces with glob pattern matching
+- Environment variables (`DEBUG`, `TRACE`, etc.) overriding config file levels
+- Dev: human-readable output; Prod: JSON output (toggleable via `FORMAT_NODE_LOG`)
+- Browser: console output with error-level default in production
+
+### Architecture Pattern & Boundary Map
+
+```mermaid
+graph 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]
+    end
+
+    subgraph External[External Packages]
+        Pino[pino v9.x]
+        PinoPretty[pino-pretty]
+        PinoHttp[pino-http]
+        Minimatch[minimatch]
+    end
+
+    App --> Factory
+    Slackbot --> Factory
+    Slack --> Factory
+    Remark --> Factory
+
+    Factory --> LevelResolver
+    Factory --> TransportSetup
+    LevelResolver --> EnvParser
+    LevelResolver --> Minimatch
+
+    Factory --> Pino
+    TransportSetup --> PinoPretty
+
+    App --> PinoHttp
+    Slackbot --> PinoHttp
+    PinoHttp --> Factory
+```
+
+**Architecture Integration**:
+- Selected pattern: Wrapper package (`@growi/logger`) encapsulating pino configuration — mirrors universal-bunyan's role
+- Domain boundary: `@growi/logger` owns all logger creation, level resolution, and transport setup; consumer apps only call `loggerFactory(name)`
+- Existing patterns preserved: factory function signature, namespace conventions, config file structure
+- New components: `LevelResolver` (namespace-to-level matching), `TransportFactory` (dev/prod stream setup), `EnvVarParser` (env variable parsing)
+- Steering compliance: shared package in `packages/` follows monorepo conventions
+
+### Technology Stack
+
+| 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 | Replaces both morgan and express-bunyan-logger |
+| 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/ |
+
+## System Flows
+
+### Logger Creation Flow
+
+```mermaid
+sequenceDiagram
+    participant Consumer as Consumer Module
+    participant Factory as LoggerFactory
+    participant Cache as Logger Cache
+    participant Resolver as LevelResolver
+    participant Pino as pino
+
+    Consumer->>Factory: loggerFactory(namespace)
+    Factory->>Cache: lookup(namespace)
+    alt Cache hit
+        Cache-->>Factory: cached logger
+    else Cache miss
+        Factory->>Resolver: resolveLevel(namespace, config, envOverrides)
+        Resolver-->>Factory: resolved level
+        Factory->>Pino: pino(options with level, name, transport)
+        Pino-->>Factory: logger instance
+        Factory->>Cache: store(namespace, logger)
+    end
+    Factory-->>Consumer: Logger
+```
+
+### Level Resolution Flow
+
+```mermaid
+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
+```
+
+## Requirements Traceability
+
+| 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 | — |
+| 9.1–9.3 | Dependency cleanup | — (removal task) | — | — |
+| 10.1–10.3 | Backward-compatible API | LoggerFactory | `Logger` type export | — |
+
+## Components and Interfaces
+
+| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies | Contracts |
+|-----------|-------------|--------|--------------|-----------------|-----------|
+| LoggerFactory | @growi/logger / Core | Create and cache namespace-bound pino loggers | 1, 4, 8, 10 | 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 | pino-pretty (P1) | Service |
+| HttpLoggerMiddleware | apps/app, apps/slackbot-proxy | Express middleware for HTTP request logging | 6 | 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 | — | — |
+
+### @growi/logger Package
+
+#### LoggerFactory
+
+| 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**
+- Create pino logger instances with resolved level and transport configuration
+- Cache logger instances per namespace to ensure singleton behavior
+- Detect platform (Node.js vs browser) and apply appropriate configuration
+- Expose `loggerFactory(name: string): pino.Logger` as the public API
+
+**Dependencies**
+- Outbound: LevelResolver — resolve level for namespace (P0)
+- Outbound: TransportFactory — create transport options (P0)
+- External: pino v9.x — logger creation (P0)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+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;
+```
+
+- Preconditions: `initializeLoggerFactory()` called before first `loggerFactory()` call
+- Postconditions: Returns a pino.Logger bound to the namespace with resolved level
+- Invariants: Same namespace always returns the same logger instance
+
+**Implementation Notes**
+- The `initializeLoggerFactory` is called once per app at startup, receiving the merged dev/prod config
+- Browser detection: `typeof window !== 'undefined' && typeof window.document !== 'undefined'`
+- In browser mode, skip transport setup and use pino's built-in `browser` option
+- The factory is a module-level singleton (module scope cache + config)
+
+#### LevelResolver
+
+| 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**
+- Match namespace against glob patterns in config (using minimatch)
+- Match namespace against env var-derived patterns (env vars take precedence)
+- Return the most specific matching level, or the `default` level as fallback
+- Parse is done once at module initialization; resolution is per-namespace at logger creation time
+
+**Dependencies**
+- Outbound: EnvVarParser — get env-derived level map (P0)
+- External: minimatch — glob pattern matching (P0)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+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;
+}
+```
+
+- Preconditions: `config` contains a `default` key
+- Postconditions: Returns a valid pino log level string
+- Invariants: Env overrides always take precedence over config
+
+#### EnvVarParser
+
+| 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**
+- Read `process.env.DEBUG`, `process.env.TRACE`, etc.
+- Split comma-separated values into individual namespace patterns
+- Return a flat `LoggerConfig` map: `{ 'growi:*': 'debug', 'growi:service:page': 'trace' }`
+- Parsed once at module load time (not per-logger)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+/**
+ * Parse log-level environment variables into a namespace-to-level map.
+ * Reads: DEBUG, TRACE, INFO, WARN, ERROR, FATAL from process.env.
+ */
+function parseEnvLevels(): LoggerConfig;
+```
+
+- Preconditions: Environment is available (`process.env`)
+- Postconditions: Returns a map where each key is a namespace pattern and value is a level string
+- Invariants: Only the six known env vars are read; unknown vars are ignored
+
+#### TransportFactory
+
+| 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 |
+
+**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
+- Browser: return pino `browser` option config (console output, production error-level default)
+- Include `name` field in all output via pino's `name` option
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+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;
+```
+
+- Preconditions: Called during logger factory initialization
+- Postconditions: Returns valid pino options for the detected environment
+- Invariants: Browser options never include Node.js transports
+
+**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
+- Prod without FORMAT_NODE_LOG (or false): raw JSON to stdout (no transport)
+- Browser production: `{ browser: { asObject: false }, level: 'error' }`
+- Browser development: `{ browser: { asObject: false } }` (inherits resolved level)
+
+### HTTP Logging Layer
+
+#### HttpLoggerMiddleware
+
+| Field | Detail |
+|-------|--------|
+| Intent | Provide Express middleware for HTTP request/response logging via pino-http |
+| Requirements | 6.1, 6.2, 6.3, 6.4 |
+
+**Responsibilities & Constraints**
+- 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)
+
+**Dependencies**
+- External: pino-http v11.x (P0)
+- Inbound: LoggerFactory — provides base logger (P0)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+import type { RequestHandler } from 'express';
+
+/**
+ * Create Express middleware for HTTP request logging.
+ * Uses pino-http with the 'express' namespace logger.
+ */
+function createHttpLoggerMiddleware(): RequestHandler;
+```
+
+- Preconditions: LoggerFactory initialized
+- Postconditions: Returns Express middleware that logs HTTP requests
+- Invariants: Static file paths are skipped in non-production mode
+
+**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`
+
+### OpenTelemetry Layer
+
+#### DiagLoggerPinoAdapter
+
+| Field | Detail |
+|-------|--------|
+| Intent | Adapt a pino logger to the OpenTelemetry DiagLogger interface |
+| Requirements | 7.1, 7.2, 7.3 |
+
+**Responsibilities & Constraints**
+- Implement the OTel `DiagLogger` interface (`error`, `warn`, `info`, `debug`, `verbose`)
+- Map `verbose()` to pino's `trace()` level
+- Parse JSON strings in message arguments (preserving current behavior)
+- Disable `@opentelemetry/instrumentation-pino` if enabled by default
+
+**Dependencies**
+- External: pino v9.x (P0)
+- External: @opentelemetry/api (P0)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+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;
+```
+
+- Preconditions: LoggerFactory initialized, pino logger available for OTel namespace
+- Postconditions: Returns a valid DiagLogger implementation
+- Invariants: All DiagLogger methods delegate to the corresponding pino level
+
+**Implementation Notes**
+- Minimal change from current `DiagLoggerBunyanAdapter` — rename class, update import from bunyan to pino
+- `parseMessage` helper can remain largely unchanged
+- In OTel SDK configuration, replace `'@opentelemetry/instrumentation-bunyan': { enabled: false }` with `'@opentelemetry/instrumentation-pino': { enabled: false }` if the instrumentation package is present
+
+## Data Models
+
+Not applicable. This feature modifies runtime logging behavior and does not introduce or change persisted data models.
+
+## Error Handling
+
+### Error Strategy
+Logging infrastructure must be resilient — a logger failure must never crash the application.
+
+### Error Categories and Responses
+- **Missing config file**: Fall back to `{ default: 'info' }` and emit a console warning
+- **Invalid log level in config/env**: Ignore the entry and log a warning to stderr
+- **Transport initialization failure** (pino-pretty not available): Fall back to raw JSON output
+- **Logger creation failure**: Return a no-op logger that silently discards messages
+
+### Monitoring
+- Logger initialization errors are written to `process.stderr` directly (cannot use the logger itself)
+- No additional monitoring infrastructure required — this is the monitoring infrastructure
+
+## Testing Strategy
+
+### Unit Tests
+- `LevelResolver.resolveLevel()` with exact matches, glob patterns, env overrides, and fallback
+- `EnvVarParser.parseEnvLevels()` with various env var combinations
+- `TransportFactory.createTransportConfig()` for dev, prod, and browser environments
+- `LoggerFactory` caching behavior (same namespace returns same instance)
+- `DiagLoggerPinoAdapter` level mapping (verbose → trace)
+
+### Integration Tests
+- Logger factory initialization with real config files (dev and prod)
+- HTTP middleware logging with Express (verify request/response logging, route skipping)
+- End-to-end: `loggerFactory('growi:test')` produces correctly formatted output in dev mode
+
+### Migration Validation
+- Verify no imports of removed packages remain after migration
+- Verify all `package.json` files are clean of bunyan/morgan references
+- Verify existing log call sites work without modification (compile check + sample runtime test)
+
+## Migration Strategy
+
+```mermaid
+flowchart TD
+    Phase1[Phase 1: Create @growi/logger package] --> Phase2[Phase 2: Migrate apps/app]
+    Phase2 --> Phase3[Phase 3: Migrate apps/slackbot-proxy]
+    Phase3 --> Phase4[Phase 4: Migrate packages/slack and remark-attachment-refs]
+    Phase4 --> Phase5[Phase 5: Update OTel adapter]
+    Phase5 --> Phase6[Phase 6: Remove old dependencies]
+    Phase6 --> Phase7[Phase 7: Validation and cleanup]
+```
+
+- **Phase 1**: Build and test `@growi/logger` package independently
+- **Phase 2–4**: Migrate consumers one at a time; each migration is independently deployable
+- **Phase 5**: Update OTel adapter (isolated to one file + config)
+- **Phase 6**: Remove all bunyan/morgan packages from `package.json` files
+- **Phase 7**: Final grep verification, lint, build, test across monorepo
+
+Rollback: Each phase can be reverted independently by restoring the previous `loggerFactory` implementation and dependencies.

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

@@ -0,0 +1,117 @@
+# Requirements Document
+
+## Introduction
+
+GROWI currently uses bunyan as its logging library, wrapped by the custom `universal-bunyan` package (developed by WeSeek). The system provides namespace-based hierarchical logging with environment variable-driven log level control, platform detection (Node.js/Browser), and different output formatting for development and production environments. Morgan is used for HTTP request logging in development mode while `express-bunyan-logger` handles production HTTP logging.
+
+This specification covers the complete migration from bunyan to pino, replacing `universal-bunyan` with an equivalent pino-based solution, and eliminating morgan by consolidating HTTP request logging under pino. The migration must preserve all existing functionality without degradation.
+
+### Current Components to Replace
+- `bunyan` → `pino`
+- `universal-bunyan` (custom) → pino-based equivalent (official packages preferred, custom wrapper where needed)
+- `bunyan-format` → pino transport equivalent (e.g., `pino-pretty`)
+- `express-bunyan-logger` → `pino-http` or equivalent
+- `morgan` (dev only) → consolidated into pino-http
+- `browser-bunyan` / `@browser-bunyan/console-formatted-stream` → pino browser mode or equivalent
+- `@types/bunyan` → pino's built-in types
+
+## Requirements
+
+### Requirement 1: Logger Factory with Namespace Support
+
+**Objective:** As a developer, I want to create loggers with hierarchical namespace identifiers (e.g., `growi:service:page`), so that I can identify the source of log messages and control granularity per module.
+
+#### Acceptance Criteria
+1. The Logger Factory shall provide a `loggerFactory(name: string)` function that returns a logger instance bound to the given namespace.
+2. When `loggerFactory` is called multiple times with the same namespace, the Logger Factory shall return the same cached logger instance.
+3. The Logger Factory shall support colon-delimited hierarchical namespaces (e.g., `growi:crowi`, `growi:routes:login`).
+4. The Logger Factory shall maintain API compatibility so that callers use `logger.info()`, `logger.debug()`, `logger.warn()`, `logger.error()`, `logger.trace()`, and `logger.fatal()` without changes to call sites.
+
+### Requirement 2: Namespace-Based Log Level Configuration via Config Files
+
+**Objective:** As a developer, I want to define per-namespace log levels in configuration files (separate for dev and prod), so that I can fine-tune verbosity for specific modules without restarting with different env vars.
+
+#### Acceptance Criteria
+1. The Logger Factory shall load a configuration object mapping namespace patterns to log levels (e.g., `{ 'growi:service:*': 'debug', 'default': 'info' }`).
+2. The Logger Factory shall select the dev or prod configuration based on the `NODE_ENV` environment variable.
+3. The Logger Factory shall support glob pattern matching (e.g., `growi:service:*`) for namespace-to-level mapping using minimatch-compatible syntax.
+4. When no specific namespace match exists, the Logger Factory shall fall back to the `default` level defined in the configuration.
+
+### Requirement 3: Environment Variable-Based Log Level Override
+
+**Objective:** As an operator, I want to override log levels at runtime via environment variables, so that I can enable debug/trace logging for specific namespaces without modifying code or config files.
+
+#### Acceptance Criteria
+1. The Logger Factory shall read the environment variables `DEBUG`, `TRACE`, `INFO`, `WARN`, `ERROR`, and `FATAL` to parse namespace patterns.
+2. When an environment variable (e.g., `DEBUG=growi:routes:*,growi:service:page`) is set, the Logger Factory shall apply the corresponding log level to all matching namespaces.
+3. When both a config file entry and an environment variable match the same namespace, the environment variable shall take precedence.
+4. The Logger Factory shall support comma-separated namespace patterns within a single environment variable value.
+5. The Logger Factory shall support glob wildcard patterns (e.g., `growi:*`) in environment variable values.
+
+### Requirement 4: Platform-Aware Logger (Node.js and Browser)
+
+**Objective:** As a developer, I want the logger to work seamlessly in both Node.js (server) and browser (client) environments, so that I can use the same `loggerFactory` import in universal/shared code.
+
+#### Acceptance Criteria
+1. The Logger Factory shall detect the runtime environment (Node.js vs browser) and instantiate the appropriate logger implementation.
+2. While running in a browser environment, the Logger Factory shall output logs to the browser's developer console with readable formatting.
+3. While running in a browser production environment, the Logger Factory shall default to `error` level to minimize console noise.
+4. While running in a Node.js environment, the Logger Factory shall output structured logs suitable for machine parsing or human-readable formatting depending on configuration.
+
+### Requirement 5: Output Formatting (Development vs Production)
+
+**Objective:** As a developer/operator, I want distinct log output formats for development and production, so that dev logs are human-readable while production logs are structured and parseable.
+
+#### Acceptance Criteria
+1. While `NODE_ENV` is not `production`, the Logger Factory shall output human-readable formatted logs (equivalent to bunyan-format `short` mode) using pino-pretty or an equivalent transport.
+2. While `NODE_ENV` is `production`, the Logger Factory shall output structured JSON logs by default.
+3. Where the `FORMAT_NODE_LOG` environment variable is set, the Logger Factory shall respect it to toggle between formatted and raw JSON output in production (formatted by default when `FORMAT_NODE_LOG` is unset or truthy).
+4. The Logger Factory shall include the logger namespace in all log output so that the source module is identifiable.
+
+### Requirement 6: HTTP Request Logging
+
+**Objective:** As a developer/operator, I want HTTP request logging integrated with pino, so that request/response metadata is captured in a consistent format alongside application logs, eliminating the need for morgan.
+
+#### Acceptance Criteria
+1. The GROWI Server shall log HTTP requests using `pino-http` or an equivalent pino-based middleware, replacing both `morgan` (dev) and `express-bunyan-logger` (prod).
+2. While in development mode, the HTTP Logger shall skip logging for Next.js static file requests (paths starting with `/_next/static/`).
+3. The HTTP Logger shall use a logger instance obtained from the Logger Factory with the namespace `express` (or equivalent) for consistency with existing log namespaces.
+4. The HTTP Logger shall include standard HTTP metadata (method, URL, status code, response time) in log entries.
+
+### Requirement 7: OpenTelemetry Integration
+
+**Objective:** As a developer, I want the pino-based logger to integrate with OpenTelemetry diagnostics, so that observability tooling continues to function after migration.
+
+#### Acceptance Criteria
+1. The OpenTelemetry DiagLogger adapter shall be updated to wrap pino instead of bunyan.
+2. The OpenTelemetry DiagLogger adapter shall map OpenTelemetry verbose level to pino trace level.
+3. The OpenTelemetry SDK configuration shall disable pino instrumentation if an equivalent auto-instrumentation exists (analogous to the current bunyan instrumentation disable).
+
+### Requirement 8: Multi-App Consistency
+
+**Objective:** As a developer, I want all GROWI monorepo applications to use the same pino-based logging solution, so that logging behavior and configuration are consistent across the platform.
+
+#### Acceptance Criteria
+1. The `apps/app` application shall use the pino-based Logger Factory.
+2. The `apps/slackbot-proxy` application shall use the pino-based Logger Factory.
+3. The `packages/slack` package shall use the pino-based Logger Factory.
+4. The `packages/remark-attachment-refs` package shall use the pino-based Logger Factory.
+5. The Logger Factory shall be published as a shared package within the monorepo so that all consumers import from a single source.
+
+### Requirement 9: Dependency Cleanup
+
+**Objective:** As a maintainer, I want all bunyan-related and morgan dependencies removed after migration, so that the dependency tree is clean and there is no dead code.
+
+#### Acceptance Criteria
+1. When migration is complete, the monorepo shall have no references to `bunyan`, `universal-bunyan`, `bunyan-format`, `express-bunyan-logger`, `browser-bunyan`, `@browser-bunyan/console-formatted-stream`, or `@types/bunyan` in any `package.json`.
+2. When migration is complete, the monorepo shall have no references to `morgan` or `@types/morgan` in any `package.json`.
+3. When migration is complete, no source file shall contain imports or requires of the removed packages.
+
+### Requirement 10: Backward-Compatible Log API
+
+**Objective:** As a developer, I want the new logger to expose the same method signatures as the current bunyan logger, so that existing log call sites require minimal or no changes.
+
+#### Acceptance Criteria
+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.
+3. The Logger Factory shall export a TypeScript type for the logger instance that is compatible with the pino Logger type.

+ 159 - 0
.kiro/specs/migrate-logger-to-pino/research.md

@@ -0,0 +1,159 @@
+# Research & Design Decisions
+
+---
+**Purpose**: Capture discovery findings, architectural investigations, and rationale that inform the technical design.
+---
+
+## Summary
+- **Feature**: `migrate-logger-to-pino`
+- **Discovery Scope**: Complex Integration
+- **Key Findings**:
+  - Pino and bunyan share identical argument patterns (`logger.info(obj, msg)`) — no call-site changes needed
+  - No `logger.child()` or custom serializers used in GROWI — simplifies migration significantly
+  - `@opentelemetry/instrumentation-pino` supports pino `<10`; need to verify v9.x or v10 compatibility
+  - No off-the-shelf pino package replicates universal-bunyan's namespace-based level control; custom wrapper required
+
+## Research Log
+
+### Pino Core API Compatibility with Bunyan
+- **Context**: Need to confirm argument pattern compatibility to minimize call-site changes
+- **Sources Consulted**: pino GitHub docs (api.md), npm pino@10.3.1
+- **Findings**:
+  - Log level numeric values are identical: trace=10, debug=20, info=30, warn=40, error=50, fatal=60
+  - Method signature: `logger[level]([mergingObject], [message], [...interpolationValues])` — same as bunyan
+  - `name` option adds a `"name"` field to JSON output, same as bunyan
+  - `msg` is the default message key (same as bunyan), configurable via `messageKey`
+  - `pino.child(bindings, options)` works similarly to bunyan's `child()`
+- **Implications**: Call sites using `logger.info('msg')`, `logger.info({obj}, 'msg')`, `logger.error(err)` require no changes
+
+### Pino Browser Support
+- **Context**: universal-bunyan uses browser-bunyan + ConsoleFormattedStream for client-side logging
+- **Sources Consulted**: pino GitHub docs (browser.md)
+- **Findings**:
+  - Pino has built-in browser mode activated via package.json `browser` field
+  - Maps to console methods: `console.error` (fatal/error), `console.warn`, `console.info`, `console.debug`, `console.trace`
+  - `browser.asObject: true` outputs structured objects
+  - `browser.write` allows custom per-level handlers
+  - Level control works the same as Node.js (`level` option)
+  - No separate package needed (unlike browser-bunyan)
+- **Implications**: Eliminates browser-bunyan and @browser-bunyan/console-formatted-stream dependencies entirely
+
+### Pino-Pretty as Bunyan-Format Replacement
+- **Context**: universal-bunyan uses bunyan-format with `short` (dev) and `long` (prod) output modes
+- **Sources Consulted**: pino-pretty npm (v13.1.3)
+- **Findings**:
+  - Can be used as transport (worker thread) or stream (main thread)
+  - Short mode equivalent: `singleLine: true` + `ignore: 'pid,hostname'`
+  - Long mode equivalent: default multi-line output
+  - `translateTime: 'SYS:standard'` for human-readable timestamps
+  - TTY-only pattern: conditionally enable based on `process.stdout.isTTY`
+- **Implications**: Direct replacement for bunyan-format with equivalent modes
+
+### Pino-HTTP as Morgan/Express-Bunyan-Logger Replacement
+- **Context**: GROWI uses morgan (dev) and express-bunyan-logger (prod) for HTTP request logging
+- **Sources Consulted**: pino-http npm (v11.0.0)
+- **Findings**:
+  - Express middleware with `autoLogging.ignore` for route skipping (replaces morgan's `skip`)
+  - Accepts custom pino logger instance via `logger` option
+  - `customLogLevel` for status-code-based level selection
+  - `req.log` provides child logger with request context
+  - Replaces both morgan and express-bunyan-logger in a single package
+- **Implications**: Unified HTTP logging for both dev and prod, with route filtering support
+
+### Namespace-Based Level Control
+- **Context**: universal-bunyan provides namespace-to-level mapping with minimatch glob patterns and env var overrides
+- **Sources Consulted**: pino-debug (v4.0.2), pino ecosystem search
+- **Findings**:
+  - pino-debug bridges the `debug` module but doesn't provide general namespace-level control
+  - No official pino package replicates universal-bunyan's behavior
+  - Custom implementation needed: wrapper that caches pino instances per namespace, reads config + env vars, applies minimatch matching
+  - Can use pino's `level` option per-instance (set at creation time)
+- **Implications**: Must build `@growi/logger` package as a custom wrapper around pino, replacing universal-bunyan
+
+### OpenTelemetry Instrumentation
+- **Context**: GROWI has a custom DiagLogger adapter wrapping bunyan, and disables @opentelemetry/instrumentation-bunyan
+- **Sources Consulted**: @opentelemetry/instrumentation-pino npm (v0.59.0)
+- **Findings**:
+  - Supports pino `>=5.14.0 <10` — pino v10 may not be supported yet
+  - Provides trace correlation (trace_id, span_id injection) and log sending to OTel SDK
+  - GROWI's DiagLoggerBunyanAdapter pattern maps cleanly to pino (same method names)
+  - Current code disables bunyan instrumentation; equivalent disable for pino instrumentation may be needed
+- **Implications**: Pin pino to v9.x for OTel compatibility, or verify v10 support. DiagLogger adapter changes are minimal.
+
+### Existing Call-Site Analysis
+- **Context**: Need to understand what API surface is actually used to minimize migration risk
+- **Sources Consulted**: Codebase grep across all apps and packages
+- **Findings**:
+  - **No `logger.child()` usage** anywhere in the codebase
+  - **No custom serializers** registered or used
+  - **No `logger.fields` access** or other bunyan-specific APIs
+  - Call patterns: ~30% simple string, ~50% string+object, ~10% error-only, ~10% string+error
+  - All loggers created via `loggerFactory(name)` — single entry point
+- **Implications**: Migration is primarily a factory-level change; call sites need no modification
+
+## Architecture Pattern Evaluation
+
+| 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 |
+
+## Design Decisions
+
+### Decision: Create `@growi/logger` as Shared Package
+- **Context**: universal-bunyan is a custom wrapper; need equivalent for pino
+- **Alternatives Considered**:
+  1. Direct pino usage in each app — too much duplication
+  2. Fork/patch universal-bunyan for pino — complex, hard to maintain
+  3. New shared package `@growi/logger` — clean, purpose-built
+- **Selected Approach**: New `@growi/logger` package in `packages/logger/`
+- **Rationale**: Single source of truth, testable, follows monorepo patterns (like @growi/core)
+- **Trade-offs**: One more package to maintain, but replaces external dependency
+- **Follow-up**: Define package exports, ensure tree-shaking for browser builds
+
+### Decision: Pin Pino to v9.x for OpenTelemetry Compatibility
+- **Context**: @opentelemetry/instrumentation-pino supports `<10`
+- **Alternatives Considered**:
+  1. Use pino v10 and skip OTel auto-instrumentation — loses correlation
+  2. Use pino v9 for compatibility — safe choice
+  3. Use pino v10 and verify latest instrumentation support — risky
+- **Selected Approach**: Start with pino v9.x; upgrade to v10 when OTel adds support
+- **Rationale**: OTel trace correlation is valuable for production observability
+- **Trade-offs**: Miss latest pino features temporarily
+- **Follow-up**: Monitor @opentelemetry/instrumentation-pino releases for v10 support
+
+### Decision: Use pino-pretty as Transport in Development
+- **Context**: Need human-readable output for dev, JSON for prod
+- **Alternatives Considered**:
+  1. pino-pretty as transport (worker thread) — standard approach
+  2. pino-pretty as sync stream — simpler but blocks main thread
+- **Selected Approach**: Transport for async dev logging; raw JSON in production
+- **Rationale**: Transport keeps main thread clear; dev perf is less critical but the pattern is correct
+- **Trade-offs**: Slightly more complex setup
+- **Follow-up**: Verify transport works correctly with Next.js dev server
+
+### Decision: Unified HTTP Logging with pino-http
+- **Context**: Currently uses morgan (dev) and express-bunyan-logger (prod) — two different middlewares
+- **Alternatives Considered**:
+  1. Separate dev/prod middleware (maintain split) — unnecessary complexity
+  2. Single pino-http middleware for both — clean, consistent
+- **Selected Approach**: pino-http with route filtering replaces both
+- **Rationale**: Single middleware, consistent output format, built-in request context
+- **Trade-offs**: Dev output slightly different from morgan's compact format (mitigated by pino-pretty)
+- **Follow-up**: Configure `autoLogging.ignore` for `/_next/static/` paths
+
+## Risks & Mitigations
+- **OTel instrumentation compatibility with pino version** — Mitigated by pinning to v9.x
+- **Browser bundle size increase** — Pino browser mode is lightweight; monitor with build metrics
+- **Subtle log format differences** — Acceptance test comparing output before/after
+- **Missing env var behavior** — Port minimatch logic carefully with unit tests
+- **Express middleware ordering** — Ensure pino-http is added at the same point in middleware chain
+
+## References
+- [pino API docs](https://github.com/pinojs/pino/blob/main/docs/api.md)
+- [pino browser docs](https://github.com/pinojs/pino/blob/main/docs/browser.md)
+- [pino-pretty npm](https://www.npmjs.com/package/pino-pretty)
+- [pino-http npm](https://www.npmjs.com/package/pino-http)
+- [@opentelemetry/instrumentation-pino](https://www.npmjs.com/package/@opentelemetry/instrumentation-pino)
+- [universal-bunyan source](https://github.com/weseek/universal-bunyan) — current implementation reference

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

@@ -0,0 +1,22 @@
+{
+  "feature_name": "migrate-logger-to-pino",
+  "created_at": "2026-03-23T00:00:00.000Z",
+  "updated_at": "2026-03-24T00:00:00.000Z",
+  "language": "en",
+  "phase": "tasks-generated",
+  "approvals": {
+    "requirements": {
+      "generated": true,
+      "approved": true
+    },
+    "design": {
+      "generated": true,
+      "approved": true
+    },
+    "tasks": {
+      "generated": true,
+      "approved": false
+    }
+  },
+  "ready_for_implementation": false
+}

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

@@ -0,0 +1,131 @@
+# Implementation Plan
+
+- [ ] 1. Scaffold the @growi/logger shared package
+- [ ] 1.1 Initialize the package directory, package.json, and TypeScript configuration within the monorepo packages directory
+  - Create the workspace entry as `@growi/logger` with pino v9.x and minimatch as dependencies, pino-pretty as an optional peer dependency
+  - Configure TypeScript with strict mode, ESM output, and appropriate path aliases
+  - Set up the package entry points (main, types, browser) so that bundlers resolve the correct build for Node.js vs browser
+  - Add vitest configuration for unit testing within the package
+  - _Requirements: 8.5_
+
+- [ ] 1.2 Define the shared type contracts and configuration interface
+  - Define the `LoggerConfig` type representing a namespace-pattern-to-level mapping (including a `default` key)
+  - Define the `LoggerFactoryOptions` type accepted by the initialization function
+  - Export the pino `Logger` type so consumers can type-annotate their logger variables without importing pino directly
+  - _Requirements: 10.3_
+
+- [ ] 2. Implement environment variable parsing and level resolution
+- [ ] 2.1 (P) Build the environment variable parser
+  - Read the six log-level environment variables (`DEBUG`, `TRACE`, `INFO`, `WARN`, `ERROR`, `FATAL`) from the process environment
+  - Split each variable's value by commas and trim whitespace to extract individual namespace patterns
+  - Return a flat config map where each namespace pattern maps to its corresponding level string
+  - Handle edge cases: empty values, missing variables, duplicate patterns (last wins)
+  - Write unit tests covering: single variable with multiple patterns, all six variables set, no variables set, whitespace handling
+  - _Requirements: 3.1, 3.4, 3.5_
+
+- [ ] 2.2 (P) Build the level resolver with glob pattern matching
+  - Accept a namespace string, a config map, and an env-override map; return the resolved level
+  - Check env-override map first (using minimatch for glob matching), then config map, then fall back to the config `default` entry
+  - When multiple patterns match, prefer the most specific (longest non-wildcard prefix) match
+  - Write unit tests covering: exact match, glob wildcard match, env override precedence over config, fallback to default, no matching pattern
+  - _Requirements: 2.1, 2.3, 2.4, 3.2, 3.3_
+
+- [ ] 3. Implement the transport factory for dev, prod, and browser environments
+- [ ] 3.1 (P) Build the Node.js transport configuration
+  - In development mode, produce pino-pretty transport options with human-readable timestamps, hidden pid/hostname fields, and multi-line output
+  - In production mode, produce raw JSON output to stdout by default
+  - When the `FORMAT_NODE_LOG` environment variable is unset or truthy in production, produce pino-pretty transport options with long-format output instead of raw JSON
+  - Include the logger namespace (`name` field) in all output configurations
+  - Write unit tests verifying correct options for each combination of NODE_ENV and FORMAT_NODE_LOG
+  - _Requirements: 5.1, 5.2, 5.3, 5.4_
+
+- [ ] 3.2 (P) Build the browser transport configuration
+  - Detect the browser environment using window/document checks
+  - In browser development mode, produce pino browser options that output to the developer console with the resolved namespace level
+  - In browser production mode, produce pino browser options that default to `error` level to suppress non-critical console output
+  - Write unit tests verifying browser options for dev and prod scenarios
+  - _Requirements: 4.1, 4.2, 4.3, 4.4_
+
+- [ ] 4. Implement the logger factory with caching and platform detection
+- [ ] 4.1 Build the initialization and factory functions
+  - Implement `initializeLoggerFactory(options)` that stores the merged configuration, pre-parses environment overrides, and prepares the transport config
+  - Implement `loggerFactory(name)` that checks the cache for an existing logger, resolves the level via the level resolver, creates a pino instance with appropriate transport options, caches it, and returns it
+  - Detect the runtime platform (Node.js vs browser) and apply the corresponding transport configuration from the transport factory
+  - Ensure the module exports `loggerFactory` as the default export and `initializeLoggerFactory` as a named export for backward compatibility with existing import patterns
+  - Write unit tests covering: cache hit returns same instance, different namespaces return different instances, initialization stores config correctly
+  - _Requirements: 1.1, 1.2, 1.3, 1.4, 4.1, 10.1_
+
+- [ ] 5. Migrate shared packages to @growi/logger (small scope first)
+- [ ] 5.1 (P) Update packages/slack logger to use @growi/logger
+  - Replace the logger factory implementation to import from `@growi/logger` instead of universal-bunyan
+  - Update the inline config (`{ default: 'info' }`) to use the @growi/logger initialization pattern
+  - Replace bunyan type imports with the @growi/logger Logger type
+  - Add `@growi/logger` to packages/slack dependencies
+  - Run TypeScript compilation to verify no type errors
+  - _Requirements: 8.3_
+
+- [ ] 5.2 (P) Update packages/remark-attachment-refs logger to use @growi/logger
+  - Replace the logger factory implementation to import from `@growi/logger`
+  - Update configuration and type imports to match the new package
+  - Add `@growi/logger` to packages/remark-attachment-refs dependencies
+  - Run TypeScript compilation to verify no type errors
+  - _Requirements: 8.4_
+
+- [ ] 6. Migrate apps/slackbot-proxy to @growi/logger
+- [ ] 6.1 Replace the logger factory and HTTP middleware in slackbot-proxy
+  - Update the slackbot-proxy logger utility to import from `@growi/logger` and call `initializeLoggerFactory` with its existing dev/prod config
+  - Replace express-bunyan-logger and morgan usage in the server setup with pino-http middleware
+  - Replace all `import type Logger from 'bunyan'` references with the @growi/logger Logger type
+  - Add `@growi/logger` and `pino-http` to slackbot-proxy dependencies
+  - Run TypeScript compilation to verify no type errors
+  - _Requirements: 8.2, 6.1_
+
+- [ ] 7. Migrate apps/app to @growi/logger (largest scope)
+- [ ] 7.1 Replace the logger factory module in apps/app
+  - Update the apps/app logger utility to import from `@growi/logger` instead of `universal-bunyan`
+  - Call `initializeLoggerFactory` at application startup with the existing dev/prod config files (preserve current config content)
+  - Re-export `loggerFactory` as the default export so all existing consumer imports continue to work unchanged
+  - Add `@growi/logger` to apps/app dependencies and ensure pino-pretty is available for development formatting
+  - _Requirements: 8.1, 2.2_
+
+- [ ] 7.2 Replace HTTP request logging middleware in apps/app
+  - Remove the morgan middleware (development mode) and express-bunyan-logger middleware (production mode) from the Express initialization
+  - Add pino-http middleware configured with a logger from the factory using the `express` namespace
+  - Configure route skipping to exclude `/_next/static/` paths in non-production mode
+  - Verify the middleware produces log entries containing method, URL, status code, and response time
+  - _Requirements: 6.1, 6.2, 6.3, 6.4_
+
+- [ ] 7.3 Update the OpenTelemetry diagnostic logger adapter
+  - Rename the adapter class from `DiagLoggerBunyanAdapter` to `DiagLoggerPinoAdapter` and update the import to use pino types
+  - Preserve the existing `parseMessage` helper logic that parses JSON strings and merges argument objects
+  - Confirm the verbose-to-trace level mapping continues to work with pino's trace level
+  - Update the OpenTelemetry SDK configuration to disable `@opentelemetry/instrumentation-pino` instead of `@opentelemetry/instrumentation-bunyan`
+  - _Requirements: 7.1, 7.2, 7.3_
+
+- [ ] 7.4 Update all bunyan type references in apps/app source files
+  - Replace `import type Logger from 'bunyan'` with the Logger type exported from `@growi/logger` across all source files in apps/app
+  - Verify that pino's Logger type is compatible with all existing usage patterns (info, debug, warn, error, trace, fatal method calls)
+  - Run the TypeScript compiler to confirm no type errors
+  - _Requirements: 10.1, 10.2, 10.3_
+
+- [ ] 8. Remove old logging dependencies and verify cleanup
+- [ ] 8.1 Remove bunyan-related packages from all package.json files
+  - Remove `bunyan`, `universal-bunyan`, `bunyan-format`, `express-bunyan-logger`, `browser-bunyan`, `@browser-bunyan/console-formatted-stream`, `@types/bunyan` from every package.json in the monorepo
+  - Remove `morgan` and `@types/morgan` from every package.json in the monorepo
+  - Run `pnpm install` to update the lockfile and verify no broken peer dependency warnings
+  - _Requirements: 9.1, 9.2_
+
+- [ ] 8.2 Verify no residual references to removed packages
+  - Search all source files for any remaining imports or requires of the removed packages (bunyan, universal-bunyan, browser-bunyan, express-bunyan-logger, morgan, bunyan-format)
+  - Search all configuration and type definition files for stale bunyan references
+  - Fix any remaining references found during the search
+  - _Requirements: 9.3_
+
+- [ ] 9. Run full monorepo validation
+- [ ] 9.1 Execute lint, type-check, test, and build across the monorepo
+  - Run `turbo run lint --filter @growi/app` and fix any lint errors related to the migration
+  - Run `turbo run test --filter @growi/app` and verify all existing tests pass
+  - Run `turbo run build --filter @growi/app` and confirm the production build succeeds
+  - Run the same checks for slackbot-proxy and any other affected packages
+  - Verify the @growi/logger package's own tests pass
+  - _Requirements: 1.4, 8.1, 8.2, 8.3, 8.4, 10.1, 10.2_