# Adopt structured logging for faster debugging

> Replace noisy console.log and print statements with structured, queryable log events in Sentry using stable event names, dot-notated keys, and request context.

**URL:** https://sentry.io/cookbook/structure-your-logs/

---

Replace noisy console.log and print statements with structured, queryable log events — stable event names, dot-notated keys, and context that travels with every request — so the data you need to debug a failure is already there.

**Time:** 30-40 minutes | **Difficulty:** Intermediate

## What You'll Learn

- 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

**Topics:** Logs

## Before you start

**Accounts & access**

- [Sentry account](https://sentry.io/signup/) with a project

- Access to deploy code to your application

**SDKs and packages**

- Sentry SDK installed for your platform with [Logs enabled](https://docs.sentry.io/platforms/javascript/guides/node/logs/)

**Knowledge**

- Comfortable adding and editing logging calls in your codebase

- Familiarity with your application's request lifecycle

## Steps

### 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.

```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",
});
```

[How to structure a log](https://blog.sentry.io/structure-a-log/)

### 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.

```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,
});
```

[Structured log attribute patterns](https://blog.sentry.io/structure-a-log/)

### 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.

```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,
});
```

[How to structure a log](https://blog.sentry.io/structure-a-log/)

### 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.

```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",
});
```

[Sentry JavaScript logging setup](https://docs.sentry.io/platforms/javascript/guides/node/logs/)

### 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.

```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;
  },
});
```

[Sentry beforeSendLog reference](https://docs.sentry.io/platforms/javascript/guides/node/logs/)

### 8. Query structured logs in Sentry's Log Explorer

Open [Explore > Logs](https://sentry.io/orgredirect/organizations/:orgslug/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](https://docs.sentry.io/product/logs/)

## Summary

**Your logs tell the whole story.**

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

## 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](https://www.npmjs.com/package/@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.

## FAQ

**Do these patterns work outside of JavaScript?**

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.

**What's the difference between structured logging and tracing?**

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.

**How many logs should I emit per request?**

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.

**Won't wide-event logs be expensive?**

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.

**Should I use Sentry's built-in logger or a third-party logging library?**

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.

## Next Steps

- [product/logs](https://sentry.io/product/logs/)

- [Read the full blog post](https://blog.sentry.io/structure-a-log/): Dive deeper into structured logging patterns, naming conventions, and the ESLint plugin for enforcing consistency.

---

*Source: [sentry.io/cookbook/structure-your-logs/](https://sentry.io/cookbook/structure-your-logs/)*
