Files

7.5 KiB

Logging and Monitoring

This document covers structured logging, error tracking with Sentry, and observability patterns.

Critical Rules

NO console.log - Use Structured Logger

Never use console.log in production code. Always use the structured logger from @your-app/logs.

// BAD - Unstructured console logging
console.log("Order created:", orderId);
console.error("Failed to process:", error);

// GOOD - Structured logging
import { logger } from "@your-app/logs";

logger.info("Order created", {
  orderId,
  userId,
  total: order.total,
});

logger.error("Failed to process order", {
  orderId,
  error: error instanceof Error ? error.message : String(error),
  stack: error instanceof Error ? error.stack : undefined,
});

Logger API

import { logger } from "@your-app/logs";

// Log levels
logger.debug("Debug message", { context: "value" });
logger.info("Info message", { orderId, status });
logger.warn("Warning message", { userId, reason: "quota exceeded" });
logger.error("Error message", { error: err.message, stack: err.stack });

Sentry Integration

Span Tracing

Use the tracing system to monitor performance and track operations.

import { SpanPrefix, span } from "../../../lib/tracer";

// Database operations
const orders = await span(
  `${SpanPrefix.DB}GetUserOrders`,
  () => db.select().from(orderTable).where(eq(orderTable.userId, userId)),
  { userId, limit: 20 }
);

// External API calls
const response = await span(
  `${SpanPrefix.Http}FetchInventory`,
  () => inventoryClient.getStock(productIds),
  { productCount: productIds.length }
);

// Redis cache operations
const cached = await span(
  `${SpanPrefix.Redis}GetSession`,
  () => redis.get(sessionKey),
  { sessionKey }
);

SpanPrefix Constants

Use standardized prefixes for consistent Sentry categorization:

import { SpanPrefix } from "../../../lib/tracer";

const SpanPrefix = {
  /** Database operations - maps to Sentry op: db.query */
  DB: "DB.",

  /** External HTTP API calls - maps to Sentry op: http.client */
  Http: "Http.",

  /** Redis cache operations - maps to Sentry op: db.redis */
  Redis: "Redis.",

  /** AI model invocations - maps to Sentry op: ai.run */
  AI: "AI.",

  /** Generic cache operations - maps to Sentry op: cache */
  Cache: "Cache.",

  /** Queue/message operations - maps to Sentry op: queue */
  Queue: "Queue.",
} as const;

Naming Convention

// Pattern: ${SpanPrefix.Type}${Action}${Resource}

// Database
`${SpanPrefix.DB}GetUserOrders`
`${SpanPrefix.DB}BatchUpdateProducts`
`${SpanPrefix.DB}CreateOrder`

// External APIs
`${SpanPrefix.Http}FetchPaymentStatus`
`${SpanPrefix.Http}SendNotification`

// Redis
`${SpanPrefix.Redis}GetSession`
`${SpanPrefix.Redis}SetCache`

// AI
`${SpanPrefix.AI}ClassifyContent`
`${SpanPrefix.AI}GenerateResponse`

Error Capture

import { captureError } from "../../../lib/tracer";

try {
  await processOrder(orderId);
} catch (error) {
  captureError(error, {
    tags: {
      operation: "processOrder",
      orderId,
    },
    extra: {
      userId: context.user.id,
      orderStatus: order.status,
    },
  });

  throw error; // Re-throw if needed
}

Trace Context

For complex operations, use trace context to correlate logs:

import { runWithTrace, getLogId } from "../../../lib/tracer";

export async function processOrderBatch(orderIds: string[]) {
  return runWithTrace(`batch-${Date.now()}`, async () => {
    const logId = getLogId();

    logger.info("Starting batch processing", {
      logId,
      orderCount: orderIds.length
    });

    for (const orderId of orderIds) {
      await span(
        `${SpanPrefix.DB}ProcessOrder`,
        () => processSingleOrder(orderId),
        { orderId }
      );
    }

    logger.info("Batch processing complete", { logId });
  });
}

AI SDK Telemetry

When using the Vercel AI SDK, enable telemetry for token tracking:

import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

const result = await generateText({
  model: openai("gpt-4o"),
  prompt: userPrompt,
  experimental_telemetry: {
    isEnabled: true,
    functionId: "classify-content",
    metadata: {
      userId,
      contentLength: content.length,
    },
  },
});

Telemetry Metadata

Include relevant context in telemetry:

experimental_telemetry: {
  isEnabled: true,
  functionId: "generate-response",  // Unique identifier for this AI function
  metadata: {
    // User context
    userId: context.user.id,

    // Input metrics
    promptTokens: estimatedTokens,

    // Business context
    feature: "auto-reply",
    priority: "high",
  },
}

Error Handling Patterns

Structured Error Logging

async function processPayment(orderId: string) {
  try {
    const result = await paymentGateway.charge(orderId);

    logger.info("Payment processed", {
      orderId,
      transactionId: result.transactionId,
      amount: result.amount,
    });

    return result;
  } catch (error) {
    logger.error("Payment processing failed", {
      orderId,
      error: error instanceof Error ? error.message : String(error),
      errorCode: (error as any).code,
    });

    // Capture to Sentry with context
    captureError(error, {
      tags: { service: "payment", operation: "charge" },
      extra: { orderId },
    });

    throw new ORPCError("INTERNAL_SERVER_ERROR", {
      message: "Payment processing failed",
    });
  }
}

Batch Operation Logging

async function batchUpdateInventory(updates: InventoryUpdate[]) {
  const results: ProcessResult[] = [];

  logger.info("Starting batch inventory update", {
    updateCount: updates.length,
  });

  const processed = await Promise.allSettled(
    updates.map(update => processUpdate(update))
  );

  const successful = processed.filter(r => r.status === "fulfilled").length;
  const failed = processed.filter(r => r.status === "rejected").length;

  logger.info("Batch inventory update complete", {
    total: updates.length,
    successful,
    failed,
  });

  if (failed > 0) {
    logger.warn("Some inventory updates failed", {
      failedCount: failed,
      errors: processed
        .filter((r): r is PromiseRejectedResult => r.status === "rejected")
        .map(r => r.reason?.message || "Unknown error"),
    });
  }

  return { successful, failed };
}

Logging Best Practices

What to Log

Always log:

  • Request/response for external API calls
  • Database write operations (create, update, delete)
  • Authentication events
  • Business-critical operations
  • Errors and exceptions

Log with care (avoid sensitive data):

  • User inputs (sanitize PII)
  • Request payloads (redact secrets)

Never log:

  • Passwords or tokens
  • Credit card numbers
  • Personal identification numbers
  • API keys or secrets

Log Levels Guide

Level Use Case Example
debug Development diagnostics Variable values, flow tracing
info Normal operations Order created, user logged in
warn Recoverable issues Rate limit approaching, retry attempted
error Failures requiring attention API call failed, database error

Structured Context

Always include relevant context as structured data:

// BAD - String interpolation
logger.info(`User ${userId} created order ${orderId} for $${total}`);

// GOOD - Structured context
logger.info("Order created", {
  userId,
  orderId,
  total,
  currency: "USD",
  itemCount: items.length,
});