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, 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/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;
}
});
}
}
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