Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to your final lesson in this course.
In our last session, we set up Sentry to provide a comprehensive error tracking system. This is invaluable for being alerted when things go wrong and for getting rich context about the failure. However, to understand the full story of a request—what happened leading up to an error, or simply to trace the flow of normal operations—we need more than just error reports. We need detailed, structured logs.
Today, we will address the final learning outcome of our course: Implement structured logging for server-side operations and errors. We'll move beyond simple console.log statements to build a robust, production-grade logging system that is machine-readable, searchable, and provides deep insights into your application's behavior.
1. From console.log to Structured Logging
While console.log is useful for quick debugging, it has significant limitations in a production server environment:
- Unstructured: Log messages are just strings, making them difficult to parse, filter, or query automatically.
- Inconsistent: Different developers might log messages in different formats.
- Lacks Context: A simple log message often misses crucial information like the request ID, timestamp, or log severity.
Structured logging solves these problems by formatting logs as JSON objects with a consistent schema. This makes them easy to ingest, index, and query by log management services (like Better Stack, Datadog, or the AWS CloudWatch).
The TanStack Start documentation itself promotes this pattern.
Observability | TanStack Start React Docs
Let's look at how the TanStack Start documentation approaches logging. This will establish a baseline for the pattern we're about to implement.
Please review the section 'Environment-Specific Logging'. Notice how it creates a custom logger that outputs detailed, human-readable logs in development but switches to structured JSON for production. This is the core idea we will expand upon.
While the custom logger in the docs is a great example, for a real-world application, it's often better to use a dedicated, battle-tested logging library that offers more features like high performance, log redaction, and a rich ecosystem of tools. For this, we will use Pino.
2. Introducing Pino: A High-Performance Logger
Pino is a popular logging library for Node.js, renowned for its extremely low overhead and its "JSON first" philosophy. It's designed to be fast, making it an excellent choice for performance-critical server applications.
Let's get it set up and see its default behavior.
A Complete Guide to Pino Logging in Node.js
To begin, we'll follow the 'Getting started' section of this comprehensive Pino guide from BetterStack. This will show you how to install Pino and see its default structured output.
Read the 'Getting started with Pino' section. Install pino in your project, and you can create a temporary file to test the basic logger example. Observe the JSON output, which includes level, time, pid, hostname, and msg by default.
As you can see, Pino immediately provides structured, useful information out of the box.
3. Core Logging Concepts with Pino
To use Pino effectively, we need to understand a few of its core features: log levels, adding context, and correctly logging errors.
A Complete Guide to Pino Logging in Node.js
Now, let's dive into the features that make Pino a powerful tool for server-side diagnostics.
Please read the following three sections from the Pino guide: Log levels in Pino: Understand the different severity levels (info, warn, error, etc.) and how to set the minimum level. This is key to controlling log verbosity in different environments. Adding context to your logs: This is a critical section. Focus on how child loggers (logger.child()) are used to add persistent context (like a requestId) to a set of related log messages. Logging errors with Pino: Learn the correct way to pass a native Error object to logger.error() to ensure the full stack trace is captured in the log.
These concepts are the building blocks of our logging strategy. Child loggers, in particular, are essential for tracing the lifecycle of a single request through your system.
4. Integrating Pino into TanStack Start
Now, let's build our logging module and integrate it into the application using TanStack Start's middleware pattern.
Step 4.1: Create a Centralized Logger Module
First, create a new file at src/lib/logger.ts. This module will be responsible for creating and configuring a single, shared logger instance for our entire server.
We'll configure it to have two behaviors:
- In development: Use
pino-prettyfor colorful, human-readable output in the console. - In production: Output structured JSON to
stdout, which is standard practice for containerized applications.
First, install pino-pretty:
npm install pino-pretty --save-dev
Now, add the following code to src/lib/logger.ts:
// src/lib/logger.ts
import pino from 'pino';
const pinoOptions: pino.LoggerOptions = {
level: process.env.LOG_LEVEL || 'info',
};
// In development, use pino-pretty for human-readable logs
// In production, log as JSON
if (process.env.NODE_ENV === 'development') {
pinoOptions.transport = {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
};
}
const logger = pino(pinoOptions);
export default logger;
This setup gives us the best of both worlds: readability during development and machine-parsability in production.
Step 4.2: Implement Request Logging Middleware
One of the most powerful uses of logging is to trace every incoming HTTP request. We can achieve this elegantly using TanStack Start middleware.
Create a new file src/server/loggingMiddleware.ts:
// src/server/loggingMiddleware.ts
import { createMiddleware } from '@tanstack/start';
import { randomUUID } from 'node:crypto';
import logger from '~/lib/logger';
// Extend the route context to include our logger
declare module '@tanstack/react-router' {
interface RouteContext {
logger: pino.Logger;
}
}
export const loggingMiddleware = createMiddleware().handler(
async ({ request, next }) => {
const start = Date.now();
const requestId = request.headers.get('x-request-id') || randomUUID();
// Create a child logger with request-specific context
const reqLogger = logger.child({ requestId });
reqLogger.info({
method: request.method,
url: request.url,
userAgent: request.headers.get('user-agent'),
}, 'Request started');
// Pass the child logger down to route loaders via context
const response = await next({ logger: reqLogger });
const duration = Date.now() - start;
reqLogger.info({
status: response.status,
duration,
}, 'Request finished');
// Add request ID to the response headers for client-side correlation
response.headers.set('x-request-id', requestId);
return response;
},
);
Key points in this middleware:
- We generate a unique
requestIdfor every request. - We create a
reqLoggerusinglogger.child(), which automatically includes therequestIdin every message logged with it. - We augment the
RouteContexttype and pass thereqLoggerto downstream handlers vianext({ logger: reqLogger }). - We log the start and end of each request, including critical metadata like method, URL, status, and duration.
To apply this middleware to all server routes, you can add it to your vite.config.ts file.
// vite.config.ts
import { defineConfig } from 'vite';
import tanstack from '@tanstack/start/vite';
export default defineConfig({
plugins: [
tanstack({
server: {
middleware: [
// The path should be relative to the project root
'src/server/loggingMiddleware.ts',
],
},
}),
],
});
Step 4.3: Use the Logger in Route Loaders
With the middleware in place, you can now access the request-specific logger in any route loader via the context.
// Example: src/routes/posts/$postId.tsx
export const Route = createRoute({
path: '/posts/$postId',
// ...
loader: async ({ params, context }) => {
context.logger.info({ postId: params.postId }, 'Fetching post...');
try {
const post = await fetchPostById(params.postId);
context.logger.info('Post fetched successfully');
return { post };
} catch (error) {
context.logger.error({ err: error }, 'Failed to fetch post');
throw error; // Re-throw to trigger error boundary
}
},
});
Notice how we pass the native error object to context.logger.error. Pino will automatically serialize it, including the stack trace, under the err key.
5. Security: Redacting Sensitive Information
Production logs must never contain sensitive data like passwords, API keys, or personally identifiable information (PII). Pino provides a built-in redaction feature to automatically remove or mask this data.
A Complete Guide to Pino Logging in Node.js
This is a critical, non-negotiable step for production logging. Let's learn how to use Pino's redaction feature to keep our logs safe.
Read the section 'Keeping sensitive data out of your logs'. Understand how the redact option works, including how to specify paths to sensitive fields (e.g., 'user.password') and how to use wildcards.
Let's update our logger in src/lib/logger.ts to redact common sensitive fields.
// src/lib/logger.ts
import pino from 'pino';
const pinoOptions: pino.LoggerOptions = {
level: process.env.LOG_LEVEL || 'info',
// Add redaction rules
redact: {
paths: [
'*.password',
'*.token',
'*.apiKey',
'*.secret',
'req.headers.authorization',
'req.headers.cookie',
],
censor: '[REDACTED]',
},
};
// ... rest of the file
With this configuration, any object logged that contains a property matching these paths (e.g., user.password) will have its value automatically replaced with [REDACTED].
Conclusion
Congratulations on completing the final lesson! You have now implemented a professional-grade structured logging system that complements the error tracking you set up with Sentry. This provides a comprehensive observability stack, making your application far more robust and maintainable in a production environment.
Key Takeaways:
- Structured Logging (JSON) is the standard for modern server applications, enabling automated parsing, searching, and analysis.
- Pino is a high-performance Node.js logger that is ideal for production systems.
- Environment-specific configurations (e.g.,
pino-prettyfor development, JSON for production) improve the developer experience without sacrificing production needs. - TanStack Start Middleware is the perfect place to implement request-scoped logging, using child loggers to correlate all logs for a single request via a unique ID.
- Log redaction is a critical security measure to prevent sensitive data from ever appearing in your logs.
With error tracking and structured logging in place, you now have the foundational pillars of observability for your application. This completes our module on Production Deployment and Monitoring, and with it, our entire course. You are now equipped with the key patterns and practices to build, deploy, and maintain a modern SSR application with TanStack Start.
The next step is to apply these skills to your own projects, continuing to explore the rich ecosystems of TanStack, React, and server-side JavaScript. Well done