Observability

tsdevstack includes a complete observability stack: structured logging, Prometheus metrics, distributed tracing, and health checks. All features are enabled by default when you import ObservabilityModule.

Quick start

Import the module in your app:

import { Module } from '@nestjs/common';
import { ObservabilityModule } from '@tsdevstack/nest-common';

@Module({
  imports: [ObservabilityModule],
})
export class AppModule {}

This enables:

  • Logging - Structured JSON logs with Pino
  • Metrics - Prometheus-compatible metrics at /metrics
  • Tracing - Distributed traces to Jaeger via OpenTelemetry
  • Health - Health check endpoint at /health

Local observability stack

When running npm run dev, the following services are available:

ServiceURLPurpose
Prometheushttp://localhost:9090Metrics storage and querying
Grafanahttp://localhost:4001Dashboards and visualization
Jaegerhttp://localhost:16686Distributed tracing UI

Default Grafana credentials: admin / admin

Logging

The LoggerService provides structured JSON logging with automatic trace context injection.

Basic usage

import { Injectable } from '@nestjs/common';
import { LoggerService } from '@tsdevstack/nest-common';

@Injectable()
export class UserService {
  constructor(private readonly logger: LoggerService) {
    this.logger.setContext('UserService');
  }

  async createUser(email: string) {
    this.logger.info('Creating user', { email });

    try {
      // ... create user
      this.logger.info('User created successfully', { email, userId: user.id });
    } catch (error) {
      this.logger.error('Failed to create user', error, { email });
      throw error;
    }
  }
}

Log methods

logger.debug('Debug message', { context: 'optional' });
logger.info('Info message', { context: 'optional' });
logger.warn('Warning message', { context: 'optional' });
logger.error('Error message', error, { context: 'optional' });

Log output

In development, logs are pretty-printed:

[2024-01-15 10:30:45] INFO (auth-service): Creating user
    context: "UserService"
    email: "user@example.com"

In production, logs are JSON for log aggregation:

{
  "level": "info",
  "time": "2024-01-15T10:30:45.000Z",
  "service": "auth-service",
  "context": "UserService",
  "msg": "Creating user",
  "email": "user@example.com",
  "trace_id": "abc123...",
  "span_id": "def456..."
}

Configuration

Set LOG_LEVEL environment variable to control log verbosity:

  • debug - All logs
  • info - Info and above (default)
  • warn - Warnings and errors
  • error - Errors only

PII redaction

The logger automatically redacts sensitive data to prevent PII from appearing in logs. By default, common fields like password, email, ssn, creditCard, token, secret, and apiKey are redacted.

// This log entry:
logger.info('User login', { email: 'user@example.com', password: 'secret123' });

// Outputs:
// { "msg": "User login", "email": "[REDACTED]", "password": "[REDACTED]" }

Custom redaction paths

Configure additional paths to redact:

// In your app module
LoggerModule.forRoot({
  redactPaths: ['user.phoneNumber', 'order.paymentInfo.cardNumber'],
  redactCensor: '***',  // Custom censor string (default: '[REDACTED]')
})

Or via environment variable:

LOG_REDACT_PATHS=user.phoneNumber,order.cardNumber

Disabling default redaction

If you need full control over redaction paths:

LoggerModule.forRoot({
  disableDefaultRedaction: true,
  redactPaths: ['onlyThisField'],  // Only these paths are redacted
})

Path syntax

Redaction paths support wildcards:

PatternMatches
emailTop-level email field
*.emailemail nested at any level
user.emailSpecific nested path
data[*].ssnArray elements

Metrics

The MetricsService provides Prometheus-compatible metrics using OpenTelemetry.

Built-in metrics

These metrics are automatically collected for all HTTP requests:

MetricTypeDescription
http_request_duration_secondsHistogramRequest duration
http_requests_totalCounterTotal request count
http_active_connectionsGaugeActive connections

Metrics include labels: method, route, status_code

Accessing metrics

Each service exposes metrics at /metrics:

curl http://localhost:3001/metrics

Prometheus scrapes these endpoints automatically.

Custom metrics

import { Injectable, OnModuleInit } from '@nestjs/common';
import { MetricsService } from '@tsdevstack/nest-common';
import type { Counter } from '@opentelemetry/api';

@Injectable()
export class PaymentService implements OnModuleInit {
  private paymentsProcessed!: Counter;

  constructor(private readonly metrics: MetricsService) {}

  onModuleInit() {
    this.paymentsProcessed = this.metrics.createCounter(
      'payments_processed_total',
      { description: 'Total payments processed' }
    );
  }

  async processPayment(amount: number) {
    // ... process payment
    this.paymentsProcessed?.add(1, { status: 'success', currency: 'USD' });
  }
}

Available metric types:

// Counter - monotonically increasing value
metrics.createCounter('name', { description: '...' });

// Histogram - distribution of values
metrics.createHistogram('name', { description: '...' });

// UpDownCounter - gauge-like metric
metrics.createUpDownCounter('name', { description: '...' });

Tracing

Distributed tracing helps debug requests across services. Traces are sent to Jaeger via OpenTelemetry OTLP.

Automatic tracing

HTTP requests are automatically traced. The TracingInterceptor creates spans for each request with:

  • HTTP method and route
  • Status code
  • Duration
  • Error details (if any)

Viewing traces

  1. Make a request to your API
  2. Open Jaeger UI: http://localhost:16686
  3. Select your service from the dropdown
  4. Click "Find Traces"

Manual spans

For custom operations within a request:

import { Injectable } from '@nestjs/common';
import { trace } from '@opentelemetry/api';

@Injectable()
export class PaymentService {
  private readonly tracer = trace.getTracer('payment-service');

  async processPayment(orderId: string) {
    return this.tracer.startActiveSpan('processPayment', async (span) => {
      try {
        span.setAttribute('order.id', orderId);

        // ... process payment

        span.setStatus({ code: 1 }); // OK
        return result;
      } catch (error) {
        span.setStatus({ code: 2, message: error.message }); // ERROR
        throw error;
      } finally {
        span.end();
      }
    });
  }
}

Trace context in logs

When tracing is enabled, logs automatically include trace_id and span_id. This allows you to correlate logs with traces in your observability platform.

Health checks

The health endpoint provides service status for load balancers and orchestrators.

Endpoints

EndpointPurpose
/healthFull health check with component status
/health/pingSimple liveness check

Health check response

{
  "status": "ok",
  "timestamp": "2024-01-15T10:30:45.000Z",
  "uptime": 3600.5,
  "checks": {
    "redis": { "status": "up" },
    "memory": { "status": "up", "details": { "heapUsed": 45, "heapTotal": 128 } }
  },
  "memory": {
    "used": 45,
    "total": 128
  }
}

Status values: ok, degraded, down

Configuring health checks

ObservabilityModule.forRoot({
  health: {
    redis: true,        // Enable Redis health check
    memory: {
      heapThreshold: 0.9  // Alert at 90% heap usage
    }
  }
})

Configuration options

Customize observability features:

ObservabilityModule.forRoot({
  serviceName: 'my-service',      // Override service name
  logging: true,                   // Enable/disable logging (default: true)
  metrics: true,                   // Enable/disable metrics (default: true)
  tracing: true,                   // Enable/disable tracing (default: true)
  tracingEndpoint: 'http://jaeger:4318',  // OTLP endpoint
  health: true,                    // Enable/disable health (default: true)
})

Environment variables:

VariableDefaultDescription
SERVICE_NAMEPackage nameService identifier in logs/traces
LOG_LEVELinfoLogging verbosity
LOG_REDACT_PATHS-Additional paths to redact (comma-separated)
NODE_ENV-production enables JSON logs
OTEL_EXPORTER_OTLP_ENDPOINThttp://localhost:4318Trace collector endpoint

Infrastructure endpoints

Health and metrics endpoints are marked @Public() and excluded from API documentation (@ApiExcludeController()). They bypass authentication so load balancers and monitoring systems can access them.

These routes also bypass Kong's trust header requirement since they're infrastructure endpoints, not user-facing APIs.

Cloud environments

In production, logs are sent to your cloud provider's logging service. The JSON format produced by Pino integrates seamlessly with:

ProviderServiceFeatures
GCPCloud LoggingStructured log queries, log-based metrics, alerting
AWSCloudWatch LogsLog Insights queries, metric filters, alarms
AzureAzure MonitorLog Analytics, Kusto queries, workbooks

No code changes required - the same JSON logs work everywhere. Cloud Run, ECS, and AKS automatically capture stdout and route it to their respective logging services.

For tracing in production, configure OTEL_EXPORTER_OTLP_ENDPOINT to point to your trace collector (Cloud Trace, X-Ray, or Application Insights).