Skip to main content

Secure Logging Guidelines

Overview

For SOC 2 compliance, all logging must follow these guidelines to prevent PII leakage and sensitive data exposure.

Rules

1. Use Structured Logger

Don’t use console.log directly:
console.log("User logged in:", user.email, user.apiKey);
Use the structured logger:
import { logInfo } from "@/lib/logger";

logInfo("User logged in", {
  userId: user.id,
  organizationId: user.organizationId,
});

2. Never Log Sensitive Data

Never log:
  • Passwords (even hashed)
  • API keys (full keys - prefixes are OK)
  • Session tokens / JWTs
  • HMAC secrets
  • Credit card numbers
  • Social Security Numbers
  • Full email addresses (in production)
  • Phone numbers
  • Any PII without explicit need
Safe to log:
  • User IDs (CUIDs, not emails)
  • Organization IDs
  • Resource IDs
  • Timestamps
  • Action types
  • Status codes
  • IP addresses (for security analysis)

3. Error Handling

Don’t expose internal errors to users:
catch (error) {
  return res.json({ error: error.message }); // Might leak internals
}
Sanitize error messages:
import { logError, getSafeErrorMessage } from "@/lib/logger";

catch (error) {
  logError("Database query failed", error, { userId, action: "fetchProducts" });
  return res.json({ error: getSafeErrorMessage(error) }, { status: 500 });
}

4. Logging Levels

Use appropriate log levels: ERROR - Something broke, needs immediate attention:
logError("Payment processing failed", error, { orderId });
WARN - Something unexpected, but handled:
logWarning("Rate limit exceeded", { apiKeyId, endpoint });
INFO - Normal operations, important events:
logInfo("Product created", { productId, organizationId });
DEBUG - Detailed info (development only):
logDebug("Cache hit", { key, ttl });

5. Context is Key

Always provide context for debugging: Good:
logError("Failed to process webhook", error, {
  webhookId: webhook.id,
  provider: "stripe",
  eventType: event.type,
  attemptNumber: 3,
});
Bad:
console.error("Webhook error:", error);

Migration Checklist

To migrate existing code:
  1. Find all console.log/error/warn:
    grep -r "console\.\(log\|error\|warn\|info\)" src/
    
  2. Replace with structured logger:
    • console.log()logInfo() or logDebug()
    • console.error()logError()
    • console.warn()logWarning()
  3. Audit logged data:
    • Remove any PII
    • Remove any secrets/tokens
    • Add context objects
  4. Test:
    • Verify logs appear in Sentry
    • Verify no sensitive data in logs
    • Verify error messages are safe

Production vs Development

Development:
  • Logs go to console
  • Detailed error messages OK
  • Stack traces shown
Production:
  • Logs go to Sentry
  • Generic error messages only
  • Stack traces not sent to client
  • PII automatically redacted

Audit Log vs Application Log

Audit Logs (src/lib/audit-log.ts):
  • Security events
  • User actions
  • System changes
  • Stored in database
  • Retained for 1 year minimum
  • Immutable
Application Logs (src/lib/logger.ts):
  • Application events
  • Errors and warnings
  • Debugging info
  • Sent to Sentry
  • Retained per Sentry settings
  • Not regulatory requirement

Examples

API Route Logging

import { logError, logInfo } from "@/lib/logger";
import { auditFromRequest } from "@/lib/audit-log";

export async function POST(request: Request) {
  try {
    const { user, organizationId } = await requireAuth();

    // Application log
    logInfo("Creating new product", {
      organizationId,
      userId: user.id,
    });

    const product = await createProduct(data);

    // Audit log (compliance)
    await auditFromRequest(request, {
      organizationId,
      userId: user.id,
      action: "PRODUCT_CREATED",
      resource: "product",
      resourceId: product.id,
      status: "SUCCESS",
    });

    return NextResponse.json(product);

  } catch (error) {
    // Application error log
    logError("Product creation failed", error, {
      organizationId,
      userId: user.id,
    });

    // Audit log
    await auditFromRequest(request, {
      organizationId,
      userId: user.id,
      action: "PRODUCT_CREATED",
      resource: "product",
      status: "FAILURE",
      errorMessage: error instanceof Error ? error.message : "Unknown error",
    });

    return NextResponse.json(
      { error: getSafeErrorMessage(error) },
      { status: 500 }
    );
  }
}

Background Job Logging

import { logError, logInfo } from "@/lib/logger";

async function processBackgroundJob(jobId: string) {
  logInfo("Starting background job", { jobId });

  try {
    // ... process job
    logInfo("Background job completed", { jobId, duration: elapsed });

  } catch (error) {
    logError("Background job failed", error, {
      jobId,
      retryCount: job.retryCount,
    });

    throw error;
  }
}

Testing

Test that logging works correctly:
import { logError } from "@/lib/logger";

// This should NOT leak the API key
logError("API call failed", new Error("Invalid key: sk_test_abc123"));
// Logged as: "API call failed" with redacted key: [REDACTED_API_KEY]

Next Steps

  1. Run the migration script to find all console.log usage:
    npm run lint:logs  # To be created
    
  2. Review each instance and replace with structured logger
  3. Add automated linting rule to prevent future console.log usage
  4. Regular audit of logs to ensure no PII leakage