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/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

  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