Skip to main content

Logging

True Life includes a comprehensive logging system with structured attributes and distributed tracing support.

Quick Start

import { log, createLogger, withSpan } from "@core/logging";
 
// Simple logging
log.info("Application started");
log.error("Something failed", new Error("oops"));
 
// Module-specific logger
const logger = createLogger("banking");
logger.info("Transaction complete", { amount: 100 });
 
// Traced operations
await withSpan("process-payment", async (span) => {
	span.attributes = { orderId: "123" };
	log.info("Processing payment...");
	await processPayment();
});

Log Levels

Levels are aligned with OpenTelemetry severity:

LevelValueUse Case
trace1Very detailed debugging, loop iterations
debug5Debugging information
info9Normal operational messages
warn13Warning conditions
error17Error conditions
fatal21Critical errors causing shutdown
import { setLogLevel } from "@core/logging";
 
// Set minimum log level
setLogLevel("info"); // Only info, warn, error, fatal

Structured Attributes

Add context to logs for filtering and analysis:

log.info("User logged in", {
	userId: "123",
	ip: "192.168.1.1",
	sessionDuration: 3600,
});
 
log.error(
	"Database query failed",
	{
		query: "SELECT * FROM users",
		duration: 5000,
	},
	new Error("Connection timeout"),
);

Module-Specific Loggers

Create loggers with a fixed context:

const logger = createLogger("banking");
 
// All logs from this logger include the module name
logger.info("Transfer initiated", { from: "A", to: "B" });
logger.warn("Low balance detected", { accountId: "123" });
logger.error("Transfer failed", new Error("Insufficient funds"));

Distributed Tracing

Creating Spans

Spans create a hierarchical trace across operations:

// Async operations (most common)
await withSpan("fetch-user-data", async (span) => {
	span.attributes = { userId: "123" };
 
	// Nested spans automatically become children
	await withSpan("query-database", async () => {
		await db.query("SELECT * FROM users WHERE id = ?", ["123"]);
	});
 
	await withSpan("fetch-profile-image", async () => {
		await fetchImage("user-123.jpg");
	});
});
 
// Synchronous operations
const result = withSpanSync("calculate-total", (span) => {
	span.attributes = { itemCount: items.length };
	return items.reduce((sum, item) => sum + item.price, 0);
});

Trace Context

Traces flow from web → client → server, creating a hierarchical span tree:

Manual Context Management

For advanced use cases:

import { createTraceContext, pushContext, popContext, getCurrentContext, startSpan, endSpan } from "@core/logging";
 
// Create a new trace
const ctx = createTraceContext();
pushContext(ctx);
 
const span = startSpan("my-operation", { customAttr: "value" });
try {
	// Do work...
	endSpan(span, "ok");
} catch (error) {
	endSpan(span, "error");
	throw error;
} finally {
	popContext();
}

W3C Trace Context

For external system interoperability:

import { serializeTraceparent, parseTraceparent, getCurrentContext } from "@core/logging";
 
// Serialize for HTTP headers
const ctx = getCurrentContext();
if (ctx) {
	const traceparent = serializeTraceparent(ctx);
	// Format: "00-{traceId}-{spanId}-{flags}"
}
 
// Parse from incoming request
const incomingTraceparent = request.headers["traceparent"];
const parsedCtx = parseTraceparent(incomingTraceparent);

Complete Example

// src/modules/banking/banking.service.ts
import { createLogger, withSpan } from "@core/logging";
import * as repository from "@modules/core/banking/repository";
 
const logger = createLogger("banking");
 
export class BankingService {
	@Server
	async transfer(
		ctx: EventContext<[fromAccount: string, toAccount: string, amount: number]>,
	): Promise<TransferResult> {
		const [fromAccount, toAccount, amount] = ctx.args;
 
		return await withSpan("banking:transfer", async (span) => {
			span.attributes = { fromAccount, toAccount, amount, playerId: ctx.source };
 
			logger.info("Processing transfer", { fromAccount, toAccount, amount });
 
			try {
				const result = await withSpan("banking:validate", async () => {
					return await this.validateTransfer(fromAccount, toAccount, amount);
				});
 
				if (!result.valid) {
					logger.warn("Transfer validation failed", { reason: result.reason });
					throw new Error(result.reason);
				}
 
				const txResult = await withSpan("banking:execute", async () => {
					return await repository.executeTransfer(fromAccount, toAccount, amount);
				});
 
				logger.info("Transfer complete", { transactionId: txResult.id });
				return txResult;
			} catch (error) {
				logger.error("Transfer failed", error as Error);
				throw error;
			}
		});
	}
}

PII Redaction

The logging system automatically redacts sensitive information to prevent PII leakage in logs. This is enabled by default.

Automatic Redaction

Sensitive data is automatically detected and redacted:

// Passwords, tokens, and secrets are fully redacted
log.info("User auth", { password: "secret123", token: "abc..." });
// Output: { password: "[REDACTED]", token: "[REDACTED]" }
 
// Emails, phones, and IPs are partially redacted (preserving some context)
log.info("User info", { email: "user@example.com", phone: "555-123-4567" });
// Output: { email: "us***@***.com", phone: "***-***-4567" }
 
// SSNs and credit cards are partially redacted
log.info("Payment", { ssn: "123-45-6789", card: "4111111111111111" });
// Output: { ssn: "***-**-6789", card: "****-****-****-1111" }

Inline Message Redaction

PII embedded in log messages is also detected and redacted:

log.info("Contact user@example.com for support");
// Output: "Contact us***@***.com for support"

Connection String Redaction

Database connection strings with credentials are automatically detected and redacted:

const uri = "mongodb://admin:secret@localhost:27017/mydb";
log.info(`Connecting to ${uri}`);
// Output: "Connecting to mongodb://<credentials>@localhost:27017/mydb"
 
// Works with various database protocols
log.info(`DB: postgresql://user:pass@localhost:5432/db`);
// Output: "DB: postgresql://<credentials>@localhost:5432/db"

Safe Values

Use safeValue() to bypass redaction for values you know are safe:

import { safeValue } from "@core/logging";
 
// This system email won't be redacted
log.info("Config", { email: safeValue("no-reply@system.local") });

Custom Patterns

Add custom sensitive field patterns:

import { addSensitivePattern } from "@core/logging";
 
// Match by field name (case-insensitive substring)
addSensitivePattern("internalKey");
 
// Match by regex
addSensitivePattern(/^x-.*-secret$/i);
 
// Full configuration with strategy
addSensitivePattern({
	pattern: "playerId",
	strategy: "hash", // Creates "[HASH:abc123]" for correlation
});

Configuration

Configure redaction behavior:

import { setRedactionConfig } from "@core/logging";
 
// Disable value-based PII detection (only match field names)
setRedactionConfig({ detectPiiValues: false });
 
// Change the placeholder text
setRedactionConfig({ redactedPlaceholder: "***" });
 
// Disable redaction entirely (not recommended for production)
setRedactionConfig({ enabled: false });

Logger Configuration

Control redaction per-logger:

const logger = createLogger("debug", {
	redactPii: false, // Disable attribute redaction
	redactMessagesInline: false, // Disable message scanning
});

Built-in Sensitive Patterns

The following field names are automatically detected:

CategoryFields
Authpassword, secret, token, apiKey, bearer, jwt, privateKey
PIIssn, taxId, driversLicense, passport, nationalId
Contactemail, phone, mobile, cell
FinancialcreditCard, cardNumber, cvv, pin, bankAccount, routingNumber
NetworkipAddress, remoteIp, clientIp, macAddress
Locationaddress, streetAddress, zipCode, postalCode
PersonaldateOfBirth, dob, birthDate

Best Practices

  1. Use structured attributes - Enable filtering and analysis
  2. Create module loggers - Consistent context per module
  3. Wrap operations in spans - Enable distributed tracing
  4. Set appropriate log levels - Don't spam with trace/debug
  5. Include relevant context - IDs, amounts, counts
  6. Log errors with stack traces - Pass Error objects
  7. Trust the redaction system - Don't manually redact; let the system handle it
  8. Connection strings are auto-redacted - Database URIs with credentials are automatically detected and redacted via redactMessage()
  9. Use safeValue() sparingly - Only for values you've verified are safe