Skip to content

Logging ​

Glasswork provides structured logging with automatic request ID correlation, optimized for AWS Lambda and CloudWatch Logs Insights.

Quick Start

For a complete observability setup including logging, exception tracking, and request correlation, see the Observability Guide.

Default Behavior ​

When you don't configure logging, Glasswork:

  • In Lambda: Uses plain text logging (no ANSI colors) for cleaner CloudWatch output
  • In development: Uses colored console logging for readability

For production Lambda deployments, use Pino with the built-in configuration:

bash
npm install pino
bash
pnpm add pino
bash
yarn add pino
typescript
import pino from 'pino';
import { bootstrap } from 'glasswork/core';
import { lambdaPinoConfig } from 'glasswork/observability';

const { app } = await bootstrap(AppModule, {
  logger: {
    pino: pino(lambdaPinoConfig),
  },
});

What You Get ​

Automatic HTTP request logging:

json
{
  "level": "info",
  "requestId": "abc-123-def",
  "method": "POST",
  "path": "/api/users",
  "status": 201,
  "duration": 45,
  "msg": "HTTP Request"
}

Automatic request context via AsyncLocalStorage - use getRequestId() anywhere:

typescript
import { getRequestId } from 'glasswork/observability';

export class UserService {
  async create(data: CreateUserDto) {
    const requestId = getRequestId(); // Works automatically!
    console.log(JSON.stringify({ requestId, msg: 'Creating user' }));
  }
}

Context-Aware Logger ​

For cleaner service code, create a context-aware logger:

typescript
import pino from 'pino';
import { createContextAwarePinoLogger, lambdaPinoConfig } from 'glasswork/observability';

const basePino = pino(lambdaPinoConfig);

export const logger = createContextAwarePinoLogger({
  pino: basePino,
  service: 'user-service',
});

// In your service
logger.info('Creating user', { email: data.email });
// Output: {"requestId":"abc-123","service":"user-service","email":"...","msg":"Creating user"}

For cleaner service code, create a LoggerService that can be injected into your services:

typescript
// src/modules/common/logger.service.ts
import pino from 'pino';
import { type Logger } from 'glasswork/core';
import { createContextAwarePinoLogger, lambdaPinoConfig } from 'glasswork/observability';

const basePino = pino(lambdaPinoConfig);

export class LoggerService {
  private readonly loggers = new Map<string, Logger>();

  /** Get a context-aware logger for a service */
  for(serviceName: string): Logger {
    let logger = this.loggers.get(serviceName);
    if (!logger) {
      logger = createContextAwarePinoLogger({
        pino: basePino,
        service: serviceName,
      });
      this.loggers.set(serviceName, logger);
    }
    return logger;
  }

  /** Get the base pino logger (for bootstrap config) */
  get pino() {
    return basePino;
  }
}

export const loggerService = new LoggerService();

Register it in your module:

typescript
// src/modules/common/common.module.ts
import { defineModule } from 'glasswork/core';
import { loggerService } from './logger.service';

export const CommonModule = defineModule({
  name: 'common',
  providers: [
    { provide: 'loggerService', useValue: loggerService },
    // ... other providers
  ],
});

Use in bootstrap:

typescript
// src/server.ts
import { loggerService } from './modules/common/logger.service';

const { app } = await bootstrap(AppModule, {
  logger: { pino: loggerService.pino },
});

Now inject it into your services:

typescript
// src/modules/user/user.service.ts
import { type Logger } from 'glasswork/core';
import type { LoggerService } from '../common/logger.service';

export class UserService {
  private readonly logger: Logger;

  constructor({ loggerService }: { loggerService: LoggerService }) {
    this.logger = loggerService.for('UserService');
  }

  async create(data: CreateUserDto) {
    this.logger.info('Creating user', { email: data.email });
    // Output: {"requestId":"abc-123","service":"UserService","email":"...","msg":"Creating user"}
  }
}

Benefits:

  • Just 2 lines of boilerplate per service
  • requestId automatically included via AsyncLocalStorage
  • Service name automatically tagged
  • Shared pino instance across all services
  • Full TypeScript support

Built-in Logger in Route Handlers ​

When using createRoutes, a context-aware logger is automatically available in the handler context:

typescript
import { createRoutes } from 'glasswork/http';

export const userRoutes = createRoutes<{ userService: UserService }>(
  (router, { userService }, route) => {
    router.post('/users', ...route({
      tags: ['Users'],
      body: CreateUserDto,
      responses: { 201: UserDto },
      handler: async ({ body, logger }) => {
        // Logger automatically includes requestId and uses first tag as service name
        logger.info('Creating user', { email: body.email });
        // Output: {"requestId":"abc-123","service":"Users","email":"...","msg":"Creating user"}

        return userService.create(body);
      }
    }));
  }
);

The route handler logger:

  • Uses the route's first tag (e.g., 'Users') as the service name
  • Falls back to operationId or 'Route' if no tags defined
  • Requires logger.pino configured in bootstrap options

User and Custom Context ​

typescript
import { setRequestUser, setRequestContextValue } from 'glasswork/observability';

// In auth middleware - included in all subsequent logs
setRequestUser(user.id);

// Add any custom context
setRequestContextValue('tenantId', tenant.id);

CloudWatch Logs Insights ​

Query your structured logs:

sql
-- Find all logs for a request
fields @timestamp, service, msg
| filter requestId = "abc-123-def"
| sort @timestamp asc

-- Slow requests
fields requestId, method, path, duration
| filter msg = "HTTP Request" and duration > 1000
| sort duration desc

See CloudWatch Insights Queries for more examples.

Built-in Logger ​

For development or simple cases, use the built-in logger:

typescript
import { createLogger } from 'glasswork/core';

const logger = createLogger('UserService');
logger.info('Creating user', { email });
// Output: [UserService] Creating user { email: '...' }

Testing ​

Automatic Silent Logging in Tests ​

When NODE_ENV=test, route handlers automatically use a silent logger if no pino logger is configured. This means you don't need to configure anything - logs are automatically silenced in tests.

typescript
// In your test setup
process.env.NODE_ENV = 'test';

// Routes created directly (without bootstrap) will use silent logger
const router = new Hono();
rangerRoutes(router, { rangerService, authService });

Explicit Silent Logging with Bootstrap ​

If you use bootstrap in tests, configure a silent pino logger:

typescript
import pino from 'pino';

const { app } = await bootstrap(AppModule, {
  logger: { pino: pino({ level: 'silent' }) },
});

Note: When NODE_ENV=test and no pino logger is configured, route handlers automatically use a silent logger, so explicit configuration is optional.

Advanced Patterns ​

Getting Full Request Context ​

Access all context data, not just the request ID:

typescript
import { getRequestContext } from 'glasswork/observability';

export function getFullRequestContext() {
  const ctx = getRequestContext();
  return {
    requestId: ctx?.requestId,
    userId: ctx?.userId,
    method: ctx?.method,
    path: ctx?.path,
    ...ctx?.custom,
  };
}

Custom Context Logger ​

If you need more control than createContextAwarePinoLogger:

typescript
import pino from 'pino';
import { getRequestContext } from 'glasswork/observability';

const basePino = pino({ level: 'info' });

export const logger = {
  info(msg: string, extra?: object) {
    const ctx = getRequestContext();
    basePino.info({
      requestId: ctx?.requestId,
      userId: ctx?.userId,
      service: 'my-service',
      ...ctx?.custom,
      ...extra,
    }, msg);
  },
  error(msg: string, extra?: object) {
    const ctx = getRequestContext();
    basePino.error({
      requestId: ctx?.requestId,
      userId: ctx?.userId,
      service: 'my-service',
      ...ctx?.custom,
      ...extra,
    }, msg);
  },
};

Performance Notes ​

AsyncLocalStorage has minimal overhead (~1-2% in typical workloads). For best performance:

typescript
// ✅ Fast - structured objects
logger.info('User created', { userId, email });

// ❌ Slower - string interpolation
logger.info(`User ${userId} created with email ${email}`);

Removing Sensitive Data from Logs ​

Use logger.sanitizers to remove or replace sensitive values before Glasswork writes logs or reports exceptions. For example, this removes an email field from log metadata:

typescript
import pino from 'pino';
import { bootstrap } from 'glasswork/core';

const { app } = await bootstrap(AppModule, {
  logger: {
    pino: pino({ level: 'info' }),
    sanitizers: {
      metadata: (fields) => {
        const safe = { ...fields };
        delete safe.email;
        return safe;
      },
    },
  },
});

You can supply any of these functions:

FunctionReceivesReturns
pathA URL pathA path with sensitive parts removed or masked
messageA log or exception messageA safe message
errorAn ErrorAn error safe to log or report
metadataAn object of log or exception fieldsAn object with safe fields

The functions apply to HTTP logs, route logs, startup diagnostics, and exception tracking. Each bootstrap uses its own settings. You decide which values to remove; Glasswork does not add application-specific rules. Without sanitizers, logging continues to use its defaults.

Using the Same Settings in Your Services ​

For application services, create a logger with createContextAwarePinoLogger({ pino, sanitizers }). It also sanitizes errors and fields added through logger.child(). If you already have a Glasswork Logger, you can supply it through logger.instance in the bootstrap options.

Services you create yourself need that logger too. Pass it through the logger option when configuring email, SES webhooks, rate limiting, or exception trackers:

  • EmailConfig.logger
  • createSESWebhookHandler({ logger })
  • createRateLimitMiddleware({ logger, ...options })
  • createCloudWatchTracker({ logger }) or createConsoleTracker({ logger })

Released under the MIT License.