← Back to Cookbook

Structure your logs for faster debugging

Move from noisy, unstructured string logs to structured, queryable events with stable event names, scoped attributes, and context that travels with every request.

Features
Category Workflow
Time
30-40 minutes
Difficulty
Intermediate
Steps
8 steps

Before you start

Accounts & access
  • Sentry account with a project
  • Access to deploy code to your application
SDKs and packages
Knowledge
  • Comfortable adding and editing logging calls in your codebase
  • Familiarity with your application's request lifecycle

1
Audit your existing log statements

Search your codebase for unstructured string logs, console.log in JavaScript, print or logging calls in Python, and their equivalents in your language. You'll replace all of them with structured events. And where your codebase barely logs today, add structured logs at the milestones that matter. This is as much about filling gaps as replacing what's already there.

The examples in this recipe use JavaScript and Sentry's logger, but every pattern here — stable event names, scoped keys, wide events, and context — applies to any language and any structured logging library.

  • Replace interpolated messages. A log message like ${username} logged in creates a unique string per user that's impossible to group or query. Use a stable event name and put the variable in attributes instead.
  • Stop dumping raw objects. console.log(data.items) dumps whatever shape the object happens to have, so the schema is unknown and can drift from one call to the next. Select the specific fields you need.
  • Redact sensitive data. Logging full objects can accidentally expose PII like emails, tokens, or passwords. Choose explicit fields to avoid surprises.
  • Choose the right severity. console.log always logs at info, so a routine event and an outright failure look identical. Emit at the right level instead — info for business milestones, warn for recoverable problems, debug for temporary investigations, and error for failures — so you can filter noise from signal in production.

2
Replace log messages with stable event names

Replace your free-text log messages with stable event names that follow a predictable pattern, like domain.action. The domain is the area of your application (e.g. auth, cart, invoice), and the action describes the event (e.g. login, checkout, create).

domain.action is just one example convention. Any naming scheme works as long as you apply it consistently. This makes every log of the same type share the same message, so you can easily search and aggregate them.

For example, instead of logging "Jamie_Smith logged in" (which creates a unique message per user), log "auth.login" and put the username in the attributes. Now you can query for all login events, group by success/failure, or filter to a specific user, all from the same stable event name.

How to structure a log
JavaScript
import * as Sentry from "@sentry/node";

// Bad: unique message per user, impossible to aggregate
console.log(`${username} logged in`);

// Good: stable event name + structured attributes
Sentry.logger.info("auth.login", {
  "user.id": user.id,
  "auth.result": "success",
});

3
Pick specific fields and scope keys with dot notation

Instead of logging a whole object, pick the specific fields you need and give each one a scoped, dot-notated key like webhook.id or http.status_code. It's the log equivalent of "don't SELECT *" — you decide exactly what gets recorded, and every field becomes something you can search, filter, and group on in Sentry.

Use snake_case for attribute names, even in JavaScript, so keys stay queryable across your whole stack — your Go backend, Python worker, and JavaScript frontend all share one convention. And put units in numeric names (amount_cents, size_bytes, duration_ms) so a value's meaning is never ambiguous.

Structured log attribute patterns
JavaScript
// Scope each field with a dot-notated, snake_case key
Sentry.logger.warn("webhook.delivery", {
  "webhook.id": webhook.id,
  "webhook.destination_host": webhook.url.hostname,
  "http.status_code": response.status,
  "retry.attempt": attempt,
  "response.size_bytes": response.size,
});

4
Keep attribute values flat and predictable

Picking the right fields is only half of it — the shape of each value matters just as much. Every attribute value should be a flat primitive: a string, number, boolean, or an array of those primitives, never an object or an array of objects. Nested objects drift from one code path to the next, can't be filtered cleanly, and often smuggle in data you never meant to log.

For outcome fields, use a small, predictable set of values (e.g. success, failed, retried) instead of free-form text. Low-cardinality values like these are what let you group, count, and chart your events.

How to structure a log
JavaScript
// Avoid: object values with an unknown, drifting shape
Sentry.logger.warn("webhook.delivery", {
  webhook: webhook,       // unknown schema
  response: response,     // could contain anything
  error: error,           // might include stack traces
});

// Prefer: flat primitives + a low-cardinality result
Sentry.logger.warn("webhook.delivery", {
  "webhook.id": webhook.id,
  "webhook.result": "failed",   // groupable outcome
  "http.status_code": response.status,
  "error.code": error.code,
});

5
Apply wide-event logging at milestones

Whether your codebase is full of thin, scattered logs or barely logs at all, the target is the same: emit fewer, richer logs at meaningful milestones, such as the completion of a business action like a login, checkout, or file upload. Each log should contain all the attributes relevant to that event, including information accumulated from earlier in the request.

Think about what you would need to debug a problem at that point: Which user? What was in their cart? Did authentication succeed? How many retries? Pack all of that into a single wide event. When something goes wrong, one log tells you the whole story.

JavaScript
// One rich log at the checkout milestone
Sentry.logger.info("cart.checkout", {
  "cart.items_total": cart.items.length,
  "cart.total_value_cents": cart.totalCents,
  "cart.coupon_code": cart.couponCode || "none",
  "order.amount_cents": order.amountCents,
  "order.result": "success",
  "user.id": user.id,
  "user.plan_tier": user.planTier,
});

6
Build up context across the request

The challenge with wide-event logging is that later logs need data from earlier in the request. Instead of repeating attributes in every log call, use context to accumulate data as the request progresses.

In Sentry, call Sentry.getIsolationScope().setAttributes() (available in SDK v10.32+) to attach key-value pairs to the request's isolation scope. These attributes are automatically included on all future logs and metrics for that request. Set user information after authentication, cart data after it's loaded, and order data after checkout. Each log downstream inherits everything that came before.

Because context is broadcast to every downstream log, add only deliberate, policy-approved values — it's shared context, not a safeguard against logging sensitive data.

Sentry JavaScript logging setup
JavaScript
import * as Sentry from "@sentry/node";

// After authentication: set user context
Sentry.getIsolationScope().setAttributes({
  "user.id": user.id,
  "user.org_id": user.orgId,
  "user.plan_tier": user.planTier,
});

// After loading cart: add cart context
Sentry.getIsolationScope().setAttributes({
  "cart.items_total": cart.items.length,
  "cart.total_value_cents": cart.totalCents,
});

// The checkout log now automatically includes
// user.* and cart.* attributes from context
Sentry.logger.info("cart.checkout", {
  "order.amount_cents": order.amountCents,
  "order.result": "success",
});

7
Filter and redact sensitive data before sending

Use your SDK's hooks to control what gets sent to your logging platform. In Sentry, the beforeSendLog callback lets you drop or modify logs before they leave your application.

  • Drop debug-level logs in production. They're useful locally, but filter them out by level so they never ship.
  • Redact sensitive attributes. Strip fields like user.email or auth.token that should never be stored.
  • Sample logs. Reduce volume by dropping a percentage of low-priority logs.
Sentry beforeSendLog reference
JavaScript
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  enableLogs: true,
  beforeSendLog: (log) => {
    // Drop debug logs in production
    if (log.level === "debug") {
      return null;
    }
    // Redact sensitive attributes
    if (log.attributes?.["user.email"]) {
      delete log.attributes["user.email"];
    }
    return log;
  },
});

8
Query structured logs in Sentry's Log Explorer

Open Explore > Logs to search across all of your structured logs. Because you used stable event names and flat primitive attributes, you can now filter on any field. Click on any log to expand it and see all attributes rendered as a structured object. Even though you wrote them as flat dot-notated strings, Sentry displays them as nested key-value pairs for readability.

Because Sentry logs are trace-connected, each log is automatically linked to the request trace. Open any log and jump directly to the associated trace, errors, or Session Replay to see the full picture.

Sentry Logs documentation

That's it.

Your logs tell the whole story.

Every log carries structured, queryable attributes so when something breaks, you already have the context to understand why.

  • Audited existing log statements and identified what to replace
  • Replaced free-text messages with stable, searchable event names
  • Picked specific fields, scoped them with dot-notated keys, and kept values flat and queryable
  • Emitted fewer, richer logs at meaningful milestones
  • Filtered, redacted, and queried structured logs in Sentry

Pro tips

  • 💡 Pick a consistent event-name convention — for example, a two-part domain.action pattern (e.g. auth.login, cart.checkout, pdf.export). A consistent scheme makes it easy to query all events in a domain with a prefix search like auth.*.
  • 💡 Log at the exit of an operation, not the entry. At the entry you don't have much useful information yet. The interesting data (result, duration, side effects) comes at the end.
  • 💡 Use an ESLint plugin like @techsquidtv/eslint-plugin-structured-logging to enforce consistent log structure across your team and AI-generated code.
  • 💡 Include high-cardinality identifiers like user IDs, order IDs, and trace IDs. Modern platforms like Sentry handle high-cardinality data well and these fields are essential for debugging specific incidents.

Common pitfalls

  • ⚠️ Don't interpolate variables into the log message string. "Jamie_Smith logged in" creates unique messages that can't be grouped. Use a stable message and put the variable in attributes instead.
  • ⚠️ Don't dump entire objects as attributes. You lose control of the schema, can't filter on nested fields, and risk logging sensitive data you didn't expect to be there.
  • ⚠️ Don't use camelCase for attribute keys if you have a multi-language stack. Use snake_case consistently so attributes are queryable across all your services.
  • ⚠️ Don't skip log levels. Use debug for local development, info for business milestones, warn for recoverable problems, and error for failures. Filter debug logs out of production to reduce noise and cost.

Frequently asked questions

Yes. The concepts (stable event names, dot-notated scoped keys, flat primitive attributes, snake_case, wide-event logging, and context) are language-agnostic. The Sentry SDK provides structured logging APIs across JavaScript, Python, Go, Ruby, PHP, Java, .NET, and more. Your language's standard logging library or Sentry's logger API will work with these patterns.

Tracing measures the timing and hierarchy of operations in a request. It tells you how long things took and where time was spent. Structured logging captures the business context at key milestones. It tells you what happened and with what data. They complement each other: traces show the shape of a request, logs show the details. In Sentry, logs are trace-connected, so you can jump between them.

There's no fixed number, but aim for fewer, richer logs rather than many thin ones. A typical request might have 2-5 logs at key milestones (authentication, data loading, business action, response). Each log should be packed with relevant attributes so it tells the full story at that point.

Wide-event logging actually reduces cost in most cases because you emit fewer total logs. Instead of 20 small console.log statements, you emit 3-5 rich logs. You can further control costs by filtering debug logs out of production using beforeSendLog or your logging library's filter configuration.

Either works. Sentry's built-in logger (Sentry.logger) provides structured logging with automatic trace connection across all supported languages. Third-party libraries can offer additional features like hierarchical categories, implicit context propagation, and multi-destination routing. If you need those features, use a library with a Sentry sink. If you want simplicity, the built-in logger is a great choice.

Fix it, don't observe it.

Get started with the only application monitoring platform that empowers developers to fix application problems without compromising on velocity.