SHOPin Logo
Skip to main documentation content

Logging

How the SHOPin storefront accelerator uses structured logs for operations and security audit: BFF HTTP lines via pino-http, integrations aligned with the same rules, and the presentation app with server vs browser boundaries. Shared behaviour lives in core/logger-config (@core/logger-config). For HTTP status handling and the exception filter, see Error handling.

The snippets below are patterns (imports + minimal structure). For full wiring—constructor injection, module imports/providers, and real call flows—open the linked apps/bff, integrations/*, or apps/presentation files in the repo.


BFF: one line per HTTP request

apps/bff/src/common/logger/logger.config.ts wires pino-http so each handled request produces one response log line (health skips below). customLogLevel uses: 2xxinfo, 4xxwarn, 5xx or any response where pino-http passes errerror (so a 4xx can still log at error if err is set).

The global filter (HttpErrorFilter) does not call a logger. It sets request.__nestException so pino-http’s customProps can expose it as error on that line. attachExceptionForLog omits stack when the response status is one of the fixed client error codes the filter maps in STATUS_BODY_MAP (400, 401, 403, 404, 409, 429). For 500 and other statuses not in that map, stack may be included when the caught value is an Error.

Skipped paths (no request line): /health, /ready, /bff/health, /bff/ready.

Redaction and request shape: createRedactConfig uses REDACT_PATHS (fixed header paths plus expanded keys such as password, token, email, refreshToken) with censor [REDACTED]. Serializers log id, method, and path without query string so query parameters are not stored.

Environment (@core/logger-config):

  • LOG_LEVEL — How chatty logs are (trace, debug, info, warn, …). Both BFF and presentation read it the same way; if unset, info is used.
  • LOG_PRETTY_PRINT=true — Turn on human-readable log lines (pino-pretty) instead of compact JSON.
    • BFF: Pretty printing is on whenever this is true.
    • Presentation (Next.js): The variable means the same thing, but while you run next dev (NODE_ENV=development), pretty printing is forced off so logs stay on normal stdout and show up reliably in the dev terminal (logger.ts). In production (or any non-development build), true turns on pretty printing like on the BFF.

Correlation ID

A correlation ID is a UUID v4 that labels one request chain end-to-end. It is not defined by a single mechanism: the same value is carried as an HTTP header, and in the browser also as a cookie and sessionStorage so full page loads and server-side fetches can reuse it. Constants: correlation-id.ts — header x-correlation-id, cookie name correlationId.

On the BFF log line the field is named correlationId (customProps copies pino’s request id from genReqId). Invalid header values are rejected; a new id is generated.

LayerBehaviour
BFFReads CORRELATION_ID_HEADER when valid; else generates. See genReqId in logger.config.ts.
Presentation (server)getCorrelationId() — valid header first, then valid CORRELATION_ID_COOKIE (correlationId), else generate. bff-fetch-server.ts sends the header on BFF calls.
Presentation (client)bff-fetch-client.ts — keeps one id per tab in sessionStorage and mirrors it in the correlationId cookie so a refresh or server render still sees the same id.

Auth audit

AuthLoggerService in common/auth-logging writes a nested auth object (action, outcome, optional userId, ip, userAgent, correlationId, optional metadata; on failure, reason is required). action and reason enums live in auth-logging.types.ts. Levels: successinfo, failurewarn. Do not put email or secrets in these payloads (types and authLogContextFromRequest avoid email). Call sites include auth.controller.ts, customer.controller.ts (password change), and token-refresh.interceptor.ts (also used by cart-token-refresh.interceptor.ts). Inject AuthLoggerService in the constructor and import authLogContextFromRequest from common/auth-logging.

TypeScript
import type { Request } from 'express'
import {
  AuthLoggerService,
  authLogContextFromRequest,
} from '../../common/auth-logging'

/** Call from a controller method after injecting AuthLoggerService — see auth.controller.ts */
export function logAuthExamples(authLogger: AuthLoggerService, req: Request) {
  authLogger.log({
    action: 'login',
    outcome: 'success',
    ...authLogContextFromRequest(req),
  })

  authLogger.log({
    action: 'login',
    outcome: 'failure',
    reason: 'invalid_credentials',
    ...authLogContextFromRequest(req),
  })
}

BFF services

Inject PinoLogger from nestjs-pino (the BFF already registers LoggerModule). Use it for extra business context; avoid duplicating the same failure that HttpErrorFilter + pino-http already attach to the request line.

TypeScript
import { Injectable } from '@nestjs/common'
import { PinoLogger } from 'nestjs-pino'

@Injectable()
export class ExampleService {
  constructor(private readonly logger: PinoLogger) {}

  async run(orderId: string) {
    this.logger.info('Operation started', { orderId })
    this.logger.warn('Using fallback after external timeout', { vendor: 'example' })
  }
}

Integrations

Use Nest’s Logger with the class name as context. Log when an error is not rethrown as an HTTP exception, or when you add context pino-http will not have (retry, fallback). If you throw NotFoundException and similar, the HTTP response line still includes the attached exception—avoid a second log for the same failure. warn for expected/recoverable cases; error for unexpected breakage. Prefer structured objects over string concatenation; never log bodies or tokens.

TypeScript
import { Injectable, Logger, NotFoundException } from '@nestjs/common'

@Injectable()
export class ExampleIntegrationService {
  private readonly logger = new Logger(ExampleIntegrationService.name)

  async getById(id: string) {
    const entity = await this.fetchFromApi(id)
    if (!entity) {
      this.logger.warn('Resource not found', { id })
      throw new NotFoundException('Not found')
    }
    return entity
  }

  private async fetchFromApi(_id: string): Promise<unknown | null> {
    return null
  }
}

Presentation app

Server-only: apps/presentation/lib/logger — use in Server Components, Server Actions, route handlers, and server-only modules. Do not import it into client components. LOG_LEVEL / LOG_PRETTY_PRINT are covered in Environment above.

Browser: Optional console (or similar) for local debugging only. Do not log tokens, full responses, or headers that may carry secrets. In production, rely on error boundaries and user-facing copy; any telemetry must be sanitised.

The server logger is only available in server code. The pattern below matches bff-fetch-server.ts (catch network errors to the BFF; the BFF still logs HTTP 4xx/5xx for responses it returns).

TypeScript
import { logger } from '@/lib/logger'
import { getCorrelationId } from '@/lib/logger/logger-context'

/** Same idea as the catch block in apps/presentation/lib/bff/core/bff-fetch-server.ts */
export async function logBffNetworkFailure(path: string, error: unknown): Promise<never> {
  const correlationId = await getCorrelationId()
  logger.error(
    {
      path,
      correlationId,
      error: error instanceof Error ? error.message : String(error),
    },
    'BFF request failed'
  )
  throw error
}

Related


Back to Validation & resilience · Back to How to work with SHOPin