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:
| Level | Value | Use Case |
|---|---|---|
trace | 1 | Very detailed debugging, loop iterations |
debug | 5 | Debugging information |
info | 9 | Normal operational messages |
warn | 13 | Warning conditions |
error | 17 | Error conditions |
fatal | 21 | Critical errors causing shutdown |
import { setLogLevel } from "@core/logging";
// Set minimum log level
setLogLevel("info"); // Only info, warn, error, fatalStructured 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:
| Category | Fields |
|---|---|
| Auth | password, secret, token, apiKey, bearer, jwt, privateKey |
| PII | ssn, taxId, driversLicense, passport, nationalId |
| Contact | email, phone, mobile, cell |
| Financial | creditCard, cardNumber, cvv, pin, bankAccount, routingNumber |
| Network | ipAddress, remoteIp, clientIp, macAddress |
| Location | address, streetAddress, zipCode, postalCode |
| Personal | dateOfBirth, dob, birthDate |
Best Practices
- Use structured attributes - Enable filtering and analysis
- Create module loggers - Consistent context per module
- Wrap operations in spans - Enable distributed tracing
- Set appropriate log levels - Don't spam with trace/debug
- Include relevant context - IDs, amounts, counts
- Log errors with stack traces - Pass Error objects
- Trust the redaction system - Don't manually redact; let the system handle it
- Connection strings are auto-redacted - Database URIs with credentials are automatically detected and redacted via
redactMessage() - Use
safeValue()sparingly - Only for values you've verified are safe