Skip to main content
Version: 4.x

Logging System

Production-ready logging with 11 phases.

Overview​

FeatureDescription
Log LevelsTRACE, DEBUG, INFO, WARN, ERROR, FATAL
Structured LoggingObjects with full metadata
TransportsConsole, File, Custom
Context ManagementChild loggers and context tracking
Data RedactionAutomatic sensitive data protection
Performance TrackingBuilt-in metrics
Query & ExportIn-memory buffer with search
Flow VisualizationEvent flow tracking
Error SuggestionsHelpful hints for common errors
Health MonitoringCPU, memory, system health
ExpressoTS v4 logging architecture: Logger core with level filter, context, and formatter flowing to console, file, and custom transports with redaction, query, health, and flow features

Quick Start​

Basic Usage​

import { provide, inject, Logger } from "@expressots/core";

@provide(MyService)
export class MyService {
constructor(@inject(Logger) private logger: Logger) {}

doSomething() {
this.logger.info("User created", { userId: 123 });
this.logger.warn("Cache miss", { key: "user:123" });
this.logger.error("Database error", new Error("Connection failed"));
}
}

Registration​

Register logger in your application:

export class App extends AppExpress {
async configureServices(): Promise<void> {
// Register Logger as Singleton
this.Provider.register(Logger, Scope.Singleton);
}
}

Log Levels​

Six log levels with intelligent filtering:

LevelValueUse CaseExample
TRACE0Ultra-detailed debug infoFunction entry/exit, variable values
DEBUG1Detailed diagnostic informationProcessing steps, state changes
INFO2General informationUser actions, system events
WARN3Warning messagesDeprecated API usage, potential issues
ERROR4Error messagesHandled exceptions, failed operations
FATAL5Fatal errorsUnrecoverable errors, system crashes

Using Log Levels​

// All log levels
this.logger.trace("Ultra detailed debug info");
this.logger.debug("Detailed diagnostic info");
this.logger.info("General information");
this.logger.warn("Warning message");
this.logger.error("Error message");
this.logger.fatal("Fatal error");

Filtering by Level​

Only logs at or above the configured level are output:

import { Logger, LogLevel } from "@expressots/core";

// Set minimum level to INFO
this.logger.configure({ level: LogLevel.INFO });

this.logger.trace("Not shown"); // Below INFO
this.logger.debug("Not shown"); // Below INFO
this.logger.info("Shown"); // INFO and above
this.logger.warn("Shown"); // Above INFO
this.logger.error("Shown"); // Above INFO

Structured Logging​

Log complex data with full TypeScript support:

Basic Structured Data​

this.logger.info("User created", {
userId: 123,
plan: "premium",
timestamp: new Date()
});

With Context​

this.logger.info("Order processed", "OrderService", {
orderId: 456,
total: 99.99,
items: 3,
paymentMethod: "credit_card"
});

Error Objects​

try {
await database.connect();
} catch (error) {
this.logger.error(
"Database connection failed",
"DatabaseService",
error as Error
);
}

Output (Development):

[ExpressoTS] 2026-01-10 10:30:45 [PID:12345] ERROR [DatabaseService] Database connection failed
├─ error: Error: Connection timeout
└─ stack:
at Database.connect (database.ts:42:15)
at DatabaseService.initialize (service.ts:18:30)

Context Management​

Using withContext()​

Temporarily set context for a single log:

this.logger.withContext("PaymentService").info("Processing payment");
// [ExpressoTS] ... INFO [PaymentService] Processing payment

this.logger.withContext("EmailService").debug("Sending welcome email");
// [ExpressoTS] ... DEBUG [EmailService] Sending welcome email

Child Loggers​

Create a child logger with persistent context:

export class UserService {
private serviceLogger: Logger;

constructor(@inject(Logger) private logger: Logger) {
// Create child with permanent context
this.serviceLogger = logger.child("UserService");
}

createUser(data: CreateUserDto) {
this.serviceLogger.info("Creating user", { email: data.email });
this.serviceLogger.debug("Validating user data");
this.serviceLogger.info("User created successfully", { userId: 123 });
}
}

Transport System​

Transports control log destinations.

Console Transport​

Development Format (Pretty, Colored)​

import { Logger, ConsoleTransport } from "@expressots/core";

this.logger.configure({
transports: [ConsoleTransport.forDevelopment()]
});

Output:

[ExpressoTS] 2026-01-10 10:30:45 [PID:12345] INFO [UserService] User created
├─ userId: 123
├─ email: "[email protected]"
└─ plan: "premium"

Production Format (JSON)​

this.logger.configure({
transports: [ConsoleTransport.forProduction()]
});

Output:

{"timestamp":"2026-01-10T10:30:45.123Z","level":"INFO","context":"UserService","message":"User created","data":{"userId":123,"email":"[email protected]","plan":"premium"}}

File Transport​

Daily Rotating Files​

import { Logger, FileTransport } from "@expressots/core";

this.logger.configure({
transports: [
ConsoleTransport.forDevelopment(),
FileTransport.daily({
directory: "logs",
filename: "app" // Creates app-YYYY-MM-DD.log
})
]
});

Size-Based Rotation​

FileTransport.rotating({
directory: "logs",
filename: "app.log",
maxSize: "10m", // Rotate at 10MB
maxFiles: 5 // Keep 5 files
})

Fixed Filename​

FileTransport.fixed({
directory: "logs",
filename: "application.log"
})

Custom Transports​

Create custom transports for databases, external services, etc.:

import { Transport, LogEntry } from "@expressots/core";

class DatabaseTransport implements Transport {
async write(entry: LogEntry): Promise<void> {
await database.logs.insert({
level: entry.level,
message: entry.message,
context: entry.context,
timestamp: entry.timestamp,
data: entry.data
});
}
}

// Use it
this.logger.configure({
transports: [
ConsoleTransport.forDevelopment(),
new DatabaseTransport()
]
});

Multiple Transports​

Log to multiple destinations simultaneously:

this.logger.configure({
transports: [
ConsoleTransport.forDevelopment(),
FileTransport.daily({ directory: "logs" }),
FileTransport.fixed({
directory: "logs/errors",
filename: "errors.log",
level: LogLevel.ERROR // Only errors
}),
new DatabaseTransport(),
new CloudWatchTransport()
]
});

Configuration​

Complete Configuration​

import { Logger, LogLevel, ConsoleTransport, FileTransport } from "@expressots/core";

const appLogger = this.Provider.get(Logger);

appLogger.configure({
// Minimum log level
level: LogLevel.DEBUG,

// Transports (where to write logs)
transports: [
ConsoleTransport.forDevelopment(),
FileTransport.daily({ directory: "logs" })
],

// Error suggestions (Phase 8)
suggestions: {
enabled: true,
showRouteSuggestions: true,
showErrorHints: true,
maxRouteSuggestions: 3,
minSimilarityThreshold: 0.3
},

// Flow visualization (Phase 7)
flow: {
enabled: true,
showVisualization: true,
trackMemory: true,
trackNested: true
},

// Health monitoring (Phase 9)
health: {
enabled: true,
interval: 30000, // Check every 30s
memoryThreshold: 80, // Alert at 80% memory
cpuThreshold: 90, // Alert at 90% CPU
alertsEnabled: true
},

// Query & export (Phase 11)
query: {
enabled: true,
bufferSize: 1000, // Keep last 1000 logs
enableExport: true
},

// Performance tracking (Phase 5)
performance: {
enabled: true,
slowThreshold: 1000 // Warn if operation > 1s
},

// Redaction (Phase 4)
redaction: {
enabled: true,
partialRedaction: true,
revealLastChars: 4
}
});

Environment-Based Configuration​

const isDevelopment = process.env.NODE_ENV !== "production";

this.logger.configure({
level: isDevelopment ? LogLevel.DEBUG : LogLevel.WARN,
transports: [
isDevelopment
? ConsoleTransport.forDevelopment()
: ConsoleTransport.forProduction(),
...(isDevelopment ? [] : [FileTransport.daily({ directory: "logs" })])
],
suggestions: { enabled: isDevelopment },
flow: { enabled: isDevelopment },
health: { enabled: !isDevelopment },
query: { enabled: isDevelopment }
});

Sensitive Data Redaction​

Automatically redact sensitive data (passwords, API keys, credit cards).

Automatic Redaction​

Redaction is enabled by default in production:

this.logger.info("User login", {
password: "secret123", // Automatically redacted
apiKey: "sk_live_abc123", // Automatically redacted
creditCard: "4532015112830366" // Automatically redacted
});

Output:

password: "••••••••"
apiKey: "••••••••"
creditCard: "••••••••"

Partial Redaction​

Show last N characters:

import { configureGlobalRedactor } from "@expressots/core";

configureGlobalRedactor({
enabled: true,
partialRedaction: true,
revealLastChars: 4
});

this.logger.info("Payment processed", {
creditCard: "4532015112830366" // Shows last 4 digits
});
// creditCard: "••••••••••••0366"

Default Patterns​

Field Patterns (40+ patterns):

  • password, passwd, pwd
  • secret, apiKey, api_key
  • token, accessToken, refreshToken
  • creditCard, cardNumber, cvv
  • ssn, socialSecurity
  • And 30+ more...

Regex Patterns (9 patterns):

  • Credit cards: /^\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}$/
  • SSN: /^\d{3}-\d{2}-\d{4}$/
  • AWS keys: /^AKIA[0-9A-Z]{16}$/
  • JWT tokens: /^eyJ[a-zA-Z0-9_-]+\./
  • Stripe keys: /^sk_live_[a-zA-Z0-9]{24,}$/
  • And more...

Custom Patterns​

Add your own sensitive fields:

configureGlobalRedactor({
enabled: true,
customFieldPatterns: [
"internalCode",
"employeeId",
"companySecret"
],
customRegexPatterns: [
{
name: "custom_id",
pattern: /^CUST-\d{8}$/,
replacement: "CUST-••••••••"
}
]
});

Whitelist​

Prevent redaction of specific fields:

configureGlobalRedactor({
enabled: true,
whitelist: [
"password_reset_token_expires", // Won't be redacted
"api_version" // Won't be redacted
]
});

Per-Transport Configuration​

Different redaction for different transports:

this.logger.configure({
transports: [
ConsoleTransport.forDevelopment({
redaction: { enabled: false } // No redaction in dev console
}),
FileTransport.daily({
directory: "logs",
redaction: { enabled: true } // Redaction in files
})
]
});

Performance Monitoring​

Track operation duration and metrics:

Basic Performance Tracking​

this.logger.withPerformance("Database Query", async () => {
return await database.users.findMany();
});
// Logs: "Database Query completed in 45ms"

Manual Timing​

const start = Date.now();

// Your operation
await processData();

this.logger.info("Operation completed", {
duration: Date.now() - start
});

Slow Operation Warnings​

this.logger.configure({
performance: {
enabled: true,
slowThreshold: 1000 // Warn if > 1 second
}
});

// Automatically warns if slow
await this.logger.withPerformance("Heavy Operation", async () => {
return await heavyComputation();
});
// ⚠️ WARNING: "Heavy Operation completed in 2500ms (slow)"

Log Grouping​

Group related logs:

this.logger.group("User Registration Flow", () => {
this.logger.info("Validating user data");
this.logger.debug("Checking email uniqueness");
this.logger.info("Creating user record");
this.logger.info("Sending welcome email");
});

Output:

┌─ User Registration Flow
│ INFO Validating user data
│ DEBUG Checking email uniqueness
│ INFO Creating user record
│ INFO Sending welcome email
└─ User Registration Flow (completed in 150ms)

Nested Groups​

this.logger.group("Order Processing", () => {
this.logger.info("Validating order");

this.logger.group("Payment Processing", () => {
this.logger.debug("Checking payment method");
this.logger.info("Charging credit card");
});

this.logger.info("Order confirmed");
});

Flow Visualization​

Visualize event flow:

Enable Flow Tracking​

this.logger.configure({
flow: {
enabled: true,
showVisualization: true,
trackMemory: true,
trackNested: true
}
});

Automatic Visualization​

When events are emitted, see the flow:

UserCreatedEvent
├── SendWelcomeEmailHandler (12ms)
├── CreateUserProfileHandler (8ms)
├── LogAnalyticsHandler (2ms)
└── NotifyAdminHandler (5ms)
Total: 27ms

Access Flow Data​

const flowData = this.logger.getFlowData(requestId);
console.log(flowData.events);
console.log(flowData.handlers);
console.log(flowData.totalDuration);

Error Suggestions​

Helpful suggestions for common errors:

Route Suggestions​

When a 404 occurs:

❌ Route not found: GET /api/usr/profile

💡 Did you mean one of these?
- GET /api/user/profile (similarity: 0.91)
- GET /api/users/profile (similarity: 0.85)
- GET /api/user/preferences (similarity: 0.72)

Error Hints​

Helpful hints for common errors:

❌ Database connection failed

💡 Common causes:
- Check DATABASE_URL environment variable
- Verify database is running
- Check network connectivity
- Verify credentials are correct

Configuration​

this.logger.configure({
suggestions: {
enabled: true,
showRouteSuggestions: true,
showErrorHints: true,
maxRouteSuggestions: 3,
minSimilarityThreshold: 0.3
}
});

Health Monitoring​

Continuous system health monitoring:

Enable Health Monitoring​

this.logger.configure({
health: {
enabled: true,
interval: 30000, // Check every 30 seconds
memoryThreshold: 80, // Alert at 80% memory usage
cpuThreshold: 90, // Alert at 90% CPU usage
alertsEnabled: true,
logLevel: "info",
alertLogLevel: "warn"
}
});

Health Metrics​

The logger automatically tracks:

  • Memory Usage: Heap used, heap total, RSS
  • CPU Usage: Average CPU load
  • Uptime: Process uptime
  • Event Loop Lag: Node.js event loop delay

Automatic Alerts:

⚠️ [Health] High memory usage: 85% (680MB / 800MB)
⚠️ [Health] High CPU usage: 92%

Manual Health Check​

const health = this.logger.getHealthMetrics();

console.log(health.memory.heapUsedPercentage); // 45
console.log(health.cpu.average); // 23
console.log(health.uptime); // 3600 (seconds)

Query & Export​

Query and export logs from in-memory buffer:

Enable Query System​

this.logger.configure({
query: {
enabled: true,
bufferSize: 1000, // Keep last 1000 logs
enableExport: true
}
});

Query Logs​

By Level​

const errors = this.logger.queryLogs({
level: LogLevel.ERROR
});

By Context​

const userServiceLogs = this.logger.queryLogs({
context: "UserService"
});

By Time Range​

const recentLogs = this.logger.queryLogs({
startTime: Date.now() - 3600000, // Last hour
endTime: Date.now()
});

By Search Term​

const searchResults = this.logger.queryLogs({
search: "authentication"
});

By Regex​

const matches = this.logger.queryLogs({
regex: /user-\d+/
});

Chainable Query API​

Build complex queries:

const results = this.logger.query()
.level(LogLevel.ERROR)
.context("PaymentService")
.search("timeout")
.startTime(Date.now() - 3600000)
.sort("desc")
.limit(10)
.execute();

Export to Markdown​

const markdown = this.logger.exportLogsToMarkdown({
query: { level: LogLevel.ERROR },
title: "Error Logs Report",
includeStats: true,
groupBy: "level"
});

// Save to file
fs.writeFileSync("error-report.md", markdown);

Generated Markdown:

# Error Logs Report

## Statistics
- Total Logs: 156
- Errors: 45
- Warnings: 67
- Info: 44

## Errors by Context
### DatabaseService (12 errors)
- 2026-01-10 10:30:45 - Connection timeout
- 2026-01-10 10:32:15 - Query failed
...

Export Options​

// Group by level
this.logger.exportLogsToMarkdown({ groupBy: "level" });

// Group by context
this.logger.exportLogsToMarkdown({ groupBy: "context" });

// Filter before export
this.logger.exportLogsToMarkdown({
query: {
level: LogLevel.ERROR,
context: "PaymentService"
}
});

Statistics​

const stats = this.logger.getLogStats();

console.log(stats.total); // 1000
console.log(stats.byLevel); // { ERROR: 45, WARN: 67, INFO: 888 }
console.log(stats.byContext); // { UserService: 234, OrderService: 156, ... }
console.log(stats.oldest); // timestamp
console.log(stats.newest); // timestamp

Clear Logs​

this.logger.clearLogs();

Request Logging Middleware​

Log HTTP requests automatically:

import { createRequestLoggingMiddleware } from "@expressots/adapter-express";

export class App extends AppExpress {
async configureServices(): Promise<void> {
const logger = this.Provider.get(Logger);

this.Middleware.add(
createRequestLoggingMiddleware(logger, {
verbosity: "detailed", // "minimal" | "standard" | "detailed"
logBody: true, // Log request body
logHeaders: false, // Log headers
slowRequestThreshold: 1000, // Warn if request > 1s
requestIdHeader: "x-request-id",
correlationIdHeader: "x-correlation-id"
})
);
}
}

Output:

→ GET /api/users/123
├─ Request ID: abc-123-def
├─ User-Agent: Mozilla/5.0...
└─ Body: {"filter":"active"}

← 200 GET /api/users/123 (45ms)
├─ Request ID: abc-123-def
└─ Body: {"id":123,"name":"John"}

Backward Compatibility​

The legacy logger API still works:

// Legacy API (still supported)
this.logger.info("Message", "ModuleName");
this.logger.warn("Warning", "ModuleName");
this.logger.error("Error", "ModuleName");
this.logger.msg("Message", "ModuleName");

// New API (recommended)
this.logger.info("Message", { userId: 123 });
this.logger.withContext("ModuleName").info("Message");

Best Practices​

1. Use Appropriate Log Levels​

// ✅ Good
this.logger.debug("User query parameters", { filter: "active" });
this.logger.info("User created", { userId: 123 });
this.logger.warn("Cache miss", { key: "user:123" });
this.logger.error("Database error", error);

// ❌ Bad
this.logger.info("Detailed debug info..."); // Use debug
this.logger.error("Warning message"); // Use warn

2. Include Contextual Data​

// ✅ Good
this.logger.error("Payment failed", {
userId: 123,
amount: 99.99,
paymentMethod: "credit_card",
errorCode: "INSUFFICIENT_FUNDS"
});

// ❌ Bad
this.logger.error("Payment failed");

3. Use Child Loggers for Services​

// ✅ Good
export class UserService {
private logger: Logger;

constructor(@inject(Logger) logger: Logger) {
this.logger = logger.child("UserService");
}

createUser() {
this.logger.info("Creating user"); // Auto-includes context
}
}

// ❌ Bad
this.logger.withContext("UserService").info(...); // Repetitive

4. Configure Per Environment​

// ✅ Good
const config = process.env.NODE_ENV === "production"
? {
level: LogLevel.WARN,
transports: [
ConsoleTransport.forProduction(),
FileTransport.daily({ directory: "logs" })
],
redaction: { enabled: true },
suggestions: { enabled: false }
}
: {
level: LogLevel.DEBUG,
transports: [ConsoleTransport.forDevelopment()],
redaction: { enabled: false },
suggestions: { enabled: true }
};

this.logger.configure(config);

5. Handle Errors Properly​

// ✅ Good
try {
await database.connect();
} catch (error) {
this.logger.error(
"Database connection failed",
"DatabaseService",
error as Error // Pass full error object
);
throw error;
}

// ❌ Bad
catch (error) {
this.logger.error("Error"); // Missing details
}

Performance Considerations​

Memory Usage​

  • Buffer Size: Default 1000 logs (~1-5MB)
  • Circular Buffer: Prevents unbounded growth
  • Production: Disable query features to save memory
// Production: Minimal memory usage
this.logger.configure({
query: { enabled: false }, // Disable buffer
health: { enabled: true }, // Keep health monitoring
flow: { enabled: false } // Disable flow tracking
});

CPU Impact​

  • Redaction: < 1ms per log entry
  • Query: O(n) where n = buffer size
  • Export: O(n) where n = logs to export
  • Health Monitoring: Minimal (every 30s)

Recommendations​

EnvironmentLevelTransportsQueryRedactionHealth
DevelopmentDEBUGConsole (pretty)✅ Enabled❌ Disabled❌ Disabled
StagingINFOConsole + File✅ Enabled✅ Enabled✅ Enabled
ProductionWARNFile only❌ Disabled✅ Enabled✅ Enabled

Troubleshooting​

Logs Not Appearing​

Check log level:

this.logger.configure({ level: LogLevel.TRACE });

Check transports:

this.logger.configure({
transports: [ConsoleTransport.forDevelopment()]
});

File Transport Not Creating Files​

Check directory exists:

mkdir -p logs
chmod 755 logs

Verify configuration:

FileTransport.daily({
directory: "logs", // Relative or absolute path
filename: "app"
})

Redaction Not Working​

Enable redaction:

import { configureGlobalRedactor } from "@expressots/core";

configureGlobalRedactor({ enabled: true });

Check patterns:

configureGlobalRedactor({
enabled: true,
customFieldPatterns: ["myField"] // Add custom fields
});

Performance Issues​

Disable expensive features in production:

this.logger.configure({
level: LogLevel.WARN,
query: { enabled: false },
flow: { enabled: false },
suggestions: { enabled: false }
});

Memory Usage High​

Reduce buffer size:

this.logger.configure({
query: {
enabled: true,
bufferSize: 100 // Smaller buffer
}
});

Or disable query entirely:

this.logger.configure({
query: { enabled: false }
});

API Reference​

Logger Methods​

MethodDescription
trace(message, context?, data?)Log TRACE level
debug(message, context?, data?)Log DEBUG level
info(message, context?, data?)Log INFO level
warn(message, context?, data?)Log WARN level
error(message, context?, data?)Log ERROR level
fatal(message, context?, data?)Log FATAL level
withContext(context)Set temporary context
child(context)Create child logger
configure(options)Configure logger
queryLogs(options?)Query log buffer
query()Create chainable query
getAllLogs()Get all logs
getLogStats()Get statistics
clearLogs()Clear log buffer
exportLogsToMarkdown(options?)Export to markdown
getHealthMetrics()Get health metrics
getFlowData(requestId)Get flow data

Configuration Options​

interface LoggerConfig {
level?: LogLevel | string;
transports?: Transport[];
suggestions?: SuggestionsConfig;
flow?: FlowConfig;
health?: HealthConfig;
query?: QueryConfig;
performance?: PerformanceConfig;
redaction?: RedactionConfig;
}

Migration from v3​

Logger is now injectable​

v3:

import console from "console";
console.log("Message");

v4:

@provide(MyService)
export class MyService {
constructor(@inject(Logger) private logger: Logger) {}

doSomething() {
this.logger.info("Message");
}
}

New log levels​

v3: info, warn, error, msg
v4: trace, debug, info, warn, error, fatal (+ legacy methods)

Structured logging​

v3:

this.logger.info("User: " + userId + " Email: " + email);

v4:

this.logger.info("User created", { userId, email });

Comparison with Other Frameworks​

FeatureExpressoTSNestJSFastify
Log Levels6 levels5 levelsCustom
Structured Logging✅ Built-in✅ Via Winston✅ Via Pino
Multiple Transports✅ Built-in✅ Via Winston✅ Via Pino
Data Redaction✅ Automatic❌ Manual❌ Manual
Query & Export✅ Built-in❌ Not available❌ Not available
Health Monitoring✅ Built-in⚠️ Via terminus⚠️ Custom
Flow Visualization✅ Built-in❌ Not available❌ Not available
Error Suggestions✅ Built-in❌ Not available❌ Not available

Testing Logger​

Unit Testing​

Test logger in isolation with mocked transports:

describe("LoggerService", () => {
let logger: Logger;
let mockTransport: jest.Mocked<Transport>;

beforeEach(() => {
mockTransport = {
write: jest.fn().mockResolvedValue(undefined),
} as any;

logger = new Logger();
logger.configure({
transports: [mockTransport],
});
});

it("should log info messages", () => {
logger.info("Test message", { data: 123 });

expect(mockTransport.write).toHaveBeenCalledWith(
expect.objectContaining({
level: "INFO",
message: "Test message",
})
);
});

it("should respect log level filtering", () => {
logger.configure({ level: LogLevel.WARN });

logger.debug("Debug message"); // Should not log
logger.warn("Warning message"); // Should log

expect(mockTransport.write).toHaveBeenCalledTimes(1);
});
});

Testing Redaction​

describe("Data Redaction", () => {
it("should redact sensitive fields", () => {
const capture = captureLogOutput();

logger.info("User login", {
password: "secret123",
});

const log = capture.logs[0];
expect(log).toContain("[email protected]");
expect(log).not.toContain("secret123");
expect(log).toContain("••••••••");
});
});

E2E Testing​

describe("Logger E2E", () => {
let app: IWebServerPublic;

beforeAll(async () => {
app = await bootstrap(App, { port: 0 });
});

afterAll(async () => {
await app.close();
});

it("should log request/response cycle", async () => {
const capture = captureLogOutput();

await fetch(`http://localhost:${await app.getPort()}/users`);

expect(capture.logs).toContainEqual(
expect.objectContaining({ context: "RequestLogger" })
);
});
});

Support the Project​

ExpressoTS is MIT-licensed open source. See the support guide to contribute.