Home Projects Portfolio Dashboard Export PDF Log in

Enhancing Observability: Implementing Structured Logging for Critical Events

Operating a modern application requires more than just functional code; it demands robust observability. Without clear insights into what's happening, especially during critical operations, debugging becomes a nightmare, and security incidents can go unnoticed. Our estrella-tour project, an application with essential user authentication, payment processing, and booking functionalities, recently faced this challenge.

The Situation

Initially, our logging strategy relied heavily on basic console.log statements. While sufficient for simple debugging during development, this approach quickly became inadequate in production. Logs were a chaotic stream of unformatted text, making it nearly impossible to filter for specific events, track user journeys, or quickly identify the root cause of issues, particularly for security-sensitive actions like logins or transactional flows like payment webhooks and reservations. We lacked a consistent way to monitor critical business and security events, leading to a reactive rather than proactive stance on potential problems.

The Descent

As the application grew, so did the volume of logs. Sifting through megabytes of unstructured text for a login failed event or a payment rejected notification was time-consuming and prone to human error. Critical data points, such as user IDs, payment statuses, or error codes, were often embedded within free-form strings, making automated parsing or aggregation by log management systems (like Vercel Logs) extremely difficult. We realized our unstructured logs were a blind spot, hindering our ability to understand user behavior, diagnose problems efficiently, and respond to security alerts promptly.

The Wake-Up Call

The need for a more sophisticated logging mechanism became undeniable. We needed logs that were not only human-readable but also machine-parseable. Specifically, for an environment like Vercel, which collects stdout logs and provides structured query capabilities, outputting JSON-formatted logs was the clear path forward. This would enable us to leverage Vercel's logging platform to filter, analyze, and alert on specific events with precision, transforming our reactive firefighting into proactive monitoring.

What I Changed

We introduced a dedicated structured logging utility in src/lib/logger.ts. This utility ensures that all critical events are logged in a consistent JSON format to stdout. The structured nature allows us to include key-value pairs for context, such as eventType, userId, transactionId, and errorMessage, making each log entry a rich data point.

Here's a simplified example of how our structured logger is implemented and used:

// src/lib/logger.ts

type LogEvent = {
  eventType: string;
  level: 'info' | 'warn' | 'error' | 'security';
  [key: string]: any; // Allow arbitrary additional context
};

export const log = (event: LogEvent) => {
  const timestamp = new Date().toISOString();
  const logEntry = { timestamp, ...event };
  console.log(JSON.stringify(logEntry));
};

// Example usage in an endpoint
import { log } from '../lib/logger';

async function handleLogin(req: Request, res: Response) {
  try {
    // ... authentication logic ...
    if (isAuthenticated) {
      log({
        eventType: 'auth.login.ok',
        level: 'security',
        userId: 'user-123',
        ipAddress: req.ip
      });
      return res.status(200).json({ message: 'Login successful' });
    } else {
      log({
        eventType: 'auth.login.failed',
        level: 'security',
        userId: 'user-123',
        reason: 'Invalid credentials',
        ipAddress: req.ip
      });
      return res.status(401).json({ message: 'Login failed' });
    }
  } catch (error) {
    log({
      eventType: 'auth.login.error',
      level: 'error',
      error: error.message,
      ipAddress: req.ip
    });
    return res.status(500).json({ message: 'Server error' });
  }
}

This log function ensures that every event is automatically timestamped and stringified into JSON before being sent to stdout. We now explicitly log events like auth.login.failed, auth.login.ok, auth.ratelimit, webhook.invalid_signature, payment.approved, payment.rejected, webhook.error, reserva.created, and reserva.mp_error, providing rich, contextual information for each.

The Technical Lesson

Structured logging isn't just about making logs pretty; it's about making them actionable. By adopting a consistent JSON format and defining explicit event types for critical actions, we've transformed our logs from a debugging chore into a powerful monitoring tool. This approach allows us to:

  • Improve Debugging: Quickly filter logs by eventType or any other structured field.
  • Enhance Security Monitoring: Easily set up alerts for suspicious activities (e.g., multiple auth.login.failed events from the same IP).
  • Gain Business Insights: Track conversion rates or identify payment processing bottlenecks by analyzing payment.approved vs. payment.rejected events.
  • Leverage Log Management: Fully utilize external log aggregation services like Vercel Logs, which can parse JSON logs for advanced querying and visualization.

The Takeaway

Don't wait for a production incident to realize your logging is insufficient. Invest in structured logging early in your project's lifecycle. Define clear event types, embed relevant context, and ensure your logging outputs are machine-readable. This small investment pays massive dividends in observability, allowing you to proactively monitor, quickly debug, and gain deeper insights into your application's health and user behavior. Your future self (and your incident response team) will thank you.


Generated with Gitvlg.com

Enhancing Observability: Implementing Structured Logging for Critical Events
p

pedro marzano

Author

Share: