Skip to content

Logging & Telemetry

How τjs provides structured logging with request tracing and debug categories.

τjs includes a flexible logging system that integrates with popular Node.js loggers (Pino, Winston) while also providing its own structured logger. The system supports:

  • Request-scoped logging with trace IDs
  • Debug categories for granular control
  • Integration with Fastify’s logger
  • Child loggers for contextual logging

The recommended approach is to use Fastify’s built-in Pino logger:

server/index.ts
import Fastify from "fastify";
import pino from "pino";
import { createServer } from "@taujs/server";
const fastify = Fastify({
logger: pino({
level: process.env.NODE_ENV === "production" ? "info" : "debug",
transport:
process.env.NODE_ENV !== "production"
? {
target: "pino-pretty",
options: { colorize: true },
}
: undefined,
}),
});
await createServer({
fastify,
config,
serviceRegistry,
clientRoot: "./client",
});

τjs automatically uses fastify.log for internal logging.

τjs’s SSR handler generates or extracts trace IDs for request correlation:

Trace ID priority:

  1. x-trace-id header (if valid)
  2. req.id from Fastify
  3. Generated via crypto.randomUUID()

Validation: Trace IDs must be alphanumeric with hyphens, underscores, dots, or colons, max 128 characters.

For SSR routes, τjs automatically adds the trace ID to response headers:

// Automatic for τjs SSR routes
'x-trace-id': 'abc123-def456-...'

In data handlers:

{
path: '/users/:id',
attr: {
render: 'ssr',
data: async (params, ctx) => {
ctx.logger.info({ userId: params.id }, 'Fetching user');
// Pass to downstream services
const res = await fetch(`/api/users/${params.id}`, {
headers: {
'x-trace-id': ctx.traceId
}
});
return await res.json();
}
}
}

In services:

export const UserService = defineService({
getUser: async (params: { id: string }, ctx) => {
ctx.logger?.info(
{ userId: params.id, traceId: ctx.traceId },
"Loading user from database"
);
const user = await db.users.findById(params.id);
return { user };
},
});

τjs supports granular debug logging through categories:

Available categories:

  • auth - Authentication hooks and verification
  • routes - Route matching and resolution
  • errors - Error handling and recovery
  • vite - Vite dev server integration
  • network - Network interface detection
  • ssr - SSR rendering pipeline

Enable all:

await createServer({
fastify,
config,
serviceRegistry,
debug: true, // or { all: true }
});

Enable specific categories:

await createServer({
fastify,
config,
serviceRegistry,
debug: ["ssr", "routes", "auth"],
});

Enable all except specific:

await createServer({
fastify,
config,
serviceRegistry,
debug: {
all: true,
vite: false,
network: false,
},
});

Using environment variables:

Terminal window
DEBUG=ssr,routes,auth npm run dev
await createServer({
fastify,
config,
serviceRegistry,
debug: process.env.DEBUG, // τjs parses comma-separated string
});

Data handlers receive a request-scoped child logger:

{
path: '/users/:id',
attr: {
render: 'ssr',
data: async (params, ctx) => {
// ctx.logger is a child logger with traceId bound
ctx.logger.info({ userId: params.id }, 'Loading user data');
try {
const user = await db.users.findById(params.id);
return { user };
} catch (err) {
ctx.logger.error({ userId: params.id, error: err }, 'Failed to load user');
throw err;
}
}
}
}

Service methods receive a child logger with context:

export const OrderService = defineService({
createOrder: async (params: { userId: string; items: any[] }, ctx) => {
ctx.logger?.info({ userId: params.userId }, "Creating order");
const order = await db.orders.create({
userId: params.userId,
items: params.items,
});
ctx.logger?.info({ orderId: order.id }, "Order created successfully");
return { order };
},
});

τjs loggers support standard levels:

// In data handlers or services
ctx.logger.debug({ detail: "value" }, "Debug message");
ctx.logger.info({ userId: "123" }, "User logged in");
ctx.logger.warn({ attempts: 3 }, "Retry limit approaching");
ctx.logger.error({ error: err }, "Operation failed");

τjs follows the pattern: logger.level(metadata, message)

// ✅ Correct - metadata first
ctx.logger.info(
{ userId: params.id, action: "login" },
"User authentication successful"
);
// ❌ Incorrect - message first
ctx.logger.info("User authentication successful", { userId: params.id });

Include relevant context in every log:

export const PaymentService = defineService({
processPayment: async (params: { orderId: string; amount: number }, ctx) => {
ctx.logger?.info(
{
orderId: params.orderId,
amount: params.amount,
userId: ctx.user?.id,
},
"Processing payment"
);
try {
const result = await paymentGateway.charge({
amount: params.amount,
orderId: params.orderId,
});
ctx.logger?.info(
{
orderId: params.orderId,
transactionId: result.id,
status: result.status,
},
"Payment processed successfully"
);
return { transaction: result };
} catch (err) {
ctx.logger?.error(
{
orderId: params.orderId,
amount: params.amount,
error: err.message,
code: err.code,
},
"Payment processing failed"
);
throw err;
}
},
});

τjs provides a Winston adapter:

import winston from "winston";
import { winstonAdapter, createServer } from "@taujs/server";
const winstonLogger = winston.createLogger({
level: "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: "app.log" }),
],
});
await createServer({
config,
serviceRegistry,
logger: winstonAdapter(winstonLogger),
debug: ["ssr", "routes"],
});

For other logging systems, create a simple adapter:

import type { BaseLogger } from "@taujs/server";
function customLoggerAdapter(customLogger: any): BaseLogger {
const wrap =
(level: "debug" | "info" | "warn" | "error") =>
(meta: Record<string, unknown>, message: string) =>
customLogger[level](message, meta);
return {
debug: wrap("debug"),
info: wrap("info"),
warn: wrap("warn"),
error: wrap("error"),
child: (ctx) =>
customLogger.child
? customLoggerAdapter(customLogger.child(ctx))
: customLoggerAdapter(customLogger),
};
}
await createServer({
config,
serviceRegistry,
logger: customLoggerAdapter(myLogger),
});
const fastify = Fastify({
logger: {
level: process.env.NODE_ENV === "production" ? "info" : "debug",
// No pretty printing in production
transport:
process.env.NODE_ENV !== "production"
? {
target: "pino-pretty",
options: { colorize: true, singleLine: false },
}
: undefined,
},
});

For high-traffic routes, sample verbose logs:

{
path: '/api/metrics',
attr: {
render: 'ssr',
data: async (params, ctx) => {
const shouldLog = Math.random() < 0.1; // 10% sampling
if (shouldLog) {
ctx.logger.debug({ route: '/api/metrics' }, 'Metrics request');
}
return { metrics: await getMetrics() };
}
}
}

Add context to errors:

export const UserService = defineService({
updateUser: async (params: { id: string; data: any }, ctx) => {
try {
const user = await db.users.update({
where: { id: params.id },
data: params.data,
});
return { user };
} catch (err) {
ctx.logger?.error(
{
kind: "database",
operation: "update",
table: "users",
userId: params.id,
error: err.message,
stack: err.stack,
traceId: ctx.traceId,
},
"Database update failed"
);
throw err;
}
},
});
fastify.decorate("authenticate", async function (req, reply) {
try {
const user = await verifyAuth(req);
req.log.info(
{
event: "auth_success",
userId: user.id,
path: req.url,
method: req.method,
},
"User authenticated"
);
req.user = user;
} catch (err) {
req.log.warn(
{
event: "auth_failure",
path: req.url,
method: req.method,
error: err.message,
},
"Authentication failed"
);
reply.code(401).send({ error: "Unauthorised" });
}
});
fastify.addHook("onRequest", (req, _reply, done) => {
(req as any).startTime = Date.now();
done();
});
fastify.addHook("onResponse", (req, reply, done) => {
const duration = Date.now() - (req as any).startTime;
req.log.info(
{
method: req.method,
url: req.url,
statusCode: reply.statusCode,
duration,
traceId: (req as any).traceId,
},
"Request completed"
);
done();
});
export const DatabaseService = defineService({
query: async (params: { sql: string; values: any[] }, ctx) => {
const start = Date.now();
try {
const result = await db.query(params.sql, params.values);
ctx.logger?.debug(
{
operation: "query",
duration: Date.now() - start,
rowCount: result.rows.length,
},
"Database query completed"
);
return { rows: result.rows };
} catch (err) {
ctx.logger?.error(
{
operation: "query",
duration: Date.now() - start,
error: err.message,
sql: params.sql,
},
"Database query failed"
);
throw err;
}
},
});
// ✅ Good - structured
ctx.logger.info(
{ userId: params.id, action: "login", ip: req.ip },
"User logged in"
);
// ❌ Bad - string interpolation
ctx.logger.info(`User ${params.id} logged in from ${req.ip}`);
// Debug - detailed flow
ctx.logger.debug({ route, params }, "Route matched");
// Info - significant events
ctx.logger.info({ userId }, "User authenticated");
// Warn - recoverable issues
ctx.logger.warn({ retries: 3 }, "API retry limit approaching");
// Error - failures
ctx.logger.error({ error, userId }, "Operation failed");
// ✅ Good - redact sensitive data
ctx.logger.info(
{
email: user.email.replace(/^(.{2}).*@/, "$1***@"),
action: "password_reset",
},
"Password reset requested"
);
// ❌ Bad - logging secrets
ctx.logger.info(
{
email: user.email,
password: params.password, // Never log passwords
token: authToken, // Never log tokens
},
"User login attempt"
);
// ✅ Good - rich context
ctx.logger.error(
{
userId: ctx.user?.id,
orderId: params.orderId,
error: err.message,
traceId: ctx.traceId,
timestamp: new Date().toISOString(),
},
"Order processing failed"
);
// ⚠️ Minimal - harder to debug
ctx.logger.error("Order failed");

Log when crossing system boundaries:

export const ExternalApiService = defineService({
fetchData: async (params: { endpoint: string }, ctx) => {
ctx.logger?.info({ endpoint: params.endpoint }, "Calling external API");
try {
const res = await fetch(`https://api.external.com${params.endpoint}`);
ctx.logger?.info(
{
endpoint: params.endpoint,
status: res.status,
duration: res.headers.get("x-response-time"),
},
"External API call completed"
);
return await res.json();
} catch (err) {
ctx.logger?.error(
{
endpoint: params.endpoint,
error: err.message,
},
"External API call failed"
);
throw err;
}
},
});