# Practical Structured Logging for Solo SaaS

> A practical setup for solo founders: how to choose what to log, structure messages so they are searchable, avoid sensitive data, and use logs for debugging without drowning in noise.

Source: https://indiefounder.me/engineering/practical-structured-logging-for-solo-saas/  
Published: 2026-11-10

When something breaks in a solo SaaS product, the first place most founders look is the logs. Unstructured log lines that say "error occurred" force you to reproduce the problem or dig through timestamps. Structured logs turn the same event into a searchable record that includes the user, the action, and the context you need to decide whether the issue is real.

This guide covers a minimal logging practice that works for early SaaS, AI tools, and developer products. It is not a full observability platform. The goal is enough information to debug production issues yourself without storing secrets or creating a second full-time job reviewing logs.

## Decide what is worth logging before you write the first line

Log events that help you answer three questions later: what the user was trying to do, what the system decided, and what failed. Typical useful events for a micro-SaaS:

- Authentication outcomes (success, failure reason such as invalid password or locked account).
- Core business actions that change state (subscription created, invoice generated, document processed, API key rotated).
- External dependency failures (payment processor timeout, email provider rejection, third-party API rate limit).
- Background job start, success, and failure with the job identifier.
- Security-relevant events (password reset requested, admin action taken, unusual login location if you track it).

Skip routine success paths that generate volume without decision value. A log entry for every page view is rarely useful at this stage and makes the useful lines harder to find. Prefer one structured event per meaningful action over many free-text lines.

Illustrative example only: a founder logs a single event when a user completes a document export, including the user identifier, document type, duration in milliseconds, and outcome. They do not log every intermediate function call inside the export routine.

## Use a consistent structure so searches work later

Structured logging means every event is a set of named fields rather than a single string. Most languages and frameworks already support this through JSON output or key-value pairs. A practical minimum set of fields:

- timestamp (ISO 8601, UTC).
- level (error, warn, info).
- event or message (a short, stable name such as subscription_created or payment_failed).
- user_id or account_id when the action is tied to a customer.
- request_id or trace_id so you can group related lines from one request.
- outcome or status (success, failed, retrying).
- duration_ms for operations that take measurable time.
- error_type or error_code when something failed (prefer a short code over a full stack trace in the main field).

Put the full stack trace or raw exception in a separate field that you can expand only when needed. Keep the primary message short and stable so you can filter on it. Avoid interpolating variable data into the message string; put variables in their own fields.

Many log aggregators and even simple cloud provider log viewers let you query by field. Once the fields are consistent, a search for event=payment_failed AND error_code=card_declined becomes possible without grepping free text.

## Never log secrets or personal data you do not need

Logs are often retained longer than you expect and are visible to more people than production databases. Before you add a field, ask whether you would be comfortable if that value appeared in a support ticket or a log export.

Do not log:

- Passwords, API keys, tokens, session secrets, or full payment card numbers.
- Full request bodies that may contain customer content or personal data.
- Email addresses or names unless the event specifically requires them and you have a retention policy that matches your privacy commitments.
- Raw third-party responses that include credentials or customer data.

Redact or hash identifiers when possible. Store the user_id you already have in your database rather than the email address. If you must log a partial value for debugging (for example the last four digits of a card for a payment failure), make it an explicit, limited field and document why it exists.

Illustrative example only: a payment failure log includes payment_id, amount, currency, error_code from the processor, and user_id. It does not include the card number, the full processor response body, or the customer’s billing address.

## Route errors and high-value events to a place you will actually look

A local log file that rotates and disappears is not enough once you have users. Choose one destination you will check when a customer reports a problem:

- The logging service built into your hosting platform (common on major cloud providers).
- A lightweight log aggregator that accepts structured JSON.
- A simple error-tracking tool that groups exceptions and notifies you.

Start with one destination. Set a retention period that matches how far back you typically need to look (many founders keep 14–30 days for routine logs and longer only for security events). Configure alerts only for conditions that require action: repeated payment failures, a spike in authentication errors, or a background job that has failed more than a fixed number of times.

Avoid alerting on every single error. A single failed webhook retry is often noise; ten failures for the same endpoint in an hour is a signal. The threshold can be a simple count you adjust after the first week of real traffic.

## Add a request identifier so you can reconstruct a single user action

When a user says “the export failed,” you need the lines that belong to that request. Generate a request_id at the start of each HTTP request or background job and include it in every log event for that unit of work. Pass the same identifier to external calls when the provider supports a correlation header.

In practice this means:

1. Middleware or a framework hook creates the request_id.
2. The identifier is added to the logging context for the duration of the request.
3. The same identifier is returned in an error response or support message so the user (or you) can reference it.
4. Background jobs receive the original request_id or generate their own and log both.

A short support reply can then say “I found the failure under request abc123; the payment processor returned a timeout.” You spend minutes instead of reconstructing the sequence from timestamps.

## Practical checklist before you ship the next logging change

- [ ] Core business actions and dependency failures have a named event and a stable set of fields.
- [ ] Every log event includes timestamp, level, event name, and request_id when it is part of a request or job.
- [ ] No passwords, tokens, full card numbers, or raw request bodies appear in log fields.
- [ ] Sensitive fields that are required are redacted or limited (last four digits, hashed identifier).
- [ ] Logs are shipped to one place you can search by field, not only by free text.
- [ ] Retention is set and you know how far back you can look.
- [ ] Alerts exist only for conditions that require you to act, with thresholds based on observed volume.
- [ ] Error responses returned to users include the request_id so support conversations can start with a concrete identifier.
- [ ] Stack traces are available in an expandable field rather than forced into every line.

## Common pitfalls

- Logging the entire request body “just in case” and later discovering it contains customer documents or personal data.
- Using free-text messages that change every time the code is edited, so historical searches stop working.
- Alerting on every exception, which trains you to ignore the channel.
- Forgetting to include the user or account identifier, so a reported problem cannot be tied to a specific customer.
- Keeping logs forever in a service that charges by volume, then receiving a large bill for data you never query.
- Writing logs only on the error path and missing the successful context that shows what the user did immediately before the failure.

## When the simple setup is no longer enough

Revisit the logging practice when:

- You spend more than a short session each week manually searching logs for routine issues.
- Multiple services or background workers make a single request_id hard to follow without a distributed tracing tool.
- Compliance or customer contracts require longer retention or specific audit events that your current fields do not capture.
- Log volume grows large enough that the cost or the noise forces you to sample or drop low-value events.
- You need to correlate logs with metrics or traces to understand latency rather than just failures.

Until then, consistent structured events, a single searchable destination, careful avoidance of secrets, and a request identifier will cover most debugging needs for a solo founder. The practice should stay small enough that you actually maintain it when you are the only person on call.

Good logs do not prevent every failure. They turn the failures that do happen into something you can understand and fix without guessing.
