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.
Before you start
Accounts & access
- Sentry account with a project
- Access to deploy code to your application
SDKs and packages
- Sentry SDK installed for your platform with Logs enabled
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 increates 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.logalways logs atinfo, so a routine event and an outright failure look identical. Emit at the right level instead —infofor business milestones,warnfor recoverable problems,debugfor temporary investigations, anderrorfor 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.
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.
// 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.
// 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.
// 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 setupimport * 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.emailorauth.tokenthat should never be stored. - Sample logs. Reduce volume by dropping a percentage of low-priority logs.
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 documentationThat'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.actionpattern (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 likeauth.*. - 💡 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
debugfor local development,infofor business milestones,warnfor recoverable problems, anderrorfor failures. Filter debug logs out of production to reduce noise and cost.
Frequently asked questions
What's next?
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.