# Replace console.log with structured logging using LogTape

> Set up LogTape with Sentry to replace console.log with structured, trace-connected logging. Query, alert on, and debug production issues with high-cardinality log data.

**URL:** https://sentry.io/cookbook/structured-logging-logtape/

---

Wire up LogTape with Sentry in a Next.js app to replace console.log with structured, trace-connected log events you can query, alert on, and tie back to the exact request that broke.

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

## What You'll Learn

- Replaced console.log with structured logging using LogTape

- Configured LogTape sinks to send logs to the console and Sentry

- Built up context throughout a user flow with high-cardinality log events

- Queried and filtered structured logs in Sentry's Log Explorer

- Created alerts and dashboards from log queries

**Topics:** Logs, Tracing, Alerts, Dashboards

**SDKs:** JavaScript, Node.js

## Before you start

**SDKs and packages**

- [@sentry/nextjs](https://docs.sentry.io/platforms/javascript/guides/nextjs/) (or your framework's Sentry SDK) installed

- [Logs enabled](https://docs.sentry.io/platforms/javascript/logs/) in your Sentry SDK config

- [@logtape/logtape](https://logtape.org/) — structured logging library

- [@logtape/sentry](https://jsr.io/@logtape/sentry) — Sentry sink for LogTape

**Accounts & access**

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

- Access to deploy code to your frontend and/or backend

**Knowledge**

- Basic JavaScript/TypeScript experience

- Familiarity with your framework's project structure (examples use Next.js)

## Steps

### 1. Install LogTape and the Sentry sink

Add LogTape and the LogTape Sentry sink package to your project. LogTape is a lightweight, zero-dependency logging library that works across Node.js, Deno, Bun, and browsers.

```bash
# npm
npm add @logtape/logtape @logtape/sentry
# pnpm
pnpm add @logtape/logtape @logtape/sentry
# yarn
yarn add @logtape/logtape @logtape/sentry
```

[LogTape installation docs](https://logtape.org/manual/install)

### 2. Configure LogTape on the client side

LogTape routes logs through **sinks** — named destinations that receive log records. In this step, you configure two: `console` for local development output and `sentry` to forward logs to your Sentry project. Loggers are grouped using **categories** — arrays like `["my-app"]` or nested like `["my-app", "checkout"]` — that form a namespace you can use to scope queries in Sentry's Log Explorer. Always initialize Sentry before calling `configure()` — the Sentry sink needs an active Sentry client to work.

```javascript
import * as Sentry from "@sentry/nextjs";
import { configure, getConsoleSink } from "@logtape/logtape";
import { getSentrySink } from "@logtape/sentry";

// 1. Initialize Sentry first
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 0.1,
  sendDefaultPii: true,
  enableLogs: true,
});

// 2. Configure LogTape with console + Sentry sinks
await configure({
  sinks: {
    console: getConsoleSink(),
    sentry: getSentrySink(),
  },
  loggers: [
    {
      category: "my-app",
      lowestLevel: "debug",
      sinks: ["console", "sentry"],
    },
  ],
});
```

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

### 3. Configure LogTape on the server side with implicit context

**Contexts** are key-value pairs attached to a logger that get included with every log it emits — without repeating them in each call. LogTape supports two models: **implicit** context on the server via Node.js `AsyncLocalStorage` (data propagates automatically through the async call chain), and **explicit** context on the client via `logger.with()` (data is attached to a derived logger you pass around). To enable implicit context on the backend, pass `contextLocalStorage: new AsyncLocalStorage()` to `configure()`. You can then wrap handler logic with `withContext()` to attach fields like `userId` or `requestId` and have every downstream log in that request automatically inherit them.

```javascript
import * as Sentry from "@sentry/nextjs";
import { configure, getConsoleSink } from "@logtape/logtape";
import { getSentrySink } from "@logtape/sentry";
import { AsyncLocalStorage } from "node:async_hooks";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.1,
  sendDefaultPii: true,
  enableLogs: true,
});

await configure({
  sinks: {
    console: getConsoleSink(),
    sentry: getSentrySink(),
  },
  loggers: [
    {
      category: "my-app",
      lowestLevel: "debug",
      sinks: ["console", "sentry"],
    },
  ],
  contextLocalStorage: new AsyncLocalStorage(),
});
```

[LogTape contexts](https://logtape.org/manual/contexts)

### 4. Use filters to control what gets sent where

In production, you may want to filter out certain log levels, like `debug`, to reduce noise. Use LogTape's `withFilter()` to wrap a sink with a minimum level — keeping verbose debug output in the console during development while only forwarding `info` and above to Sentry.

```javascript
import { configure, getConsoleSink, withFilter } from "@logtape/logtape";
import { getSentrySink } from "@logtape/sentry";

const isDev = process.env.NODE_ENV === "development";

await configure({
  sinks: {
    // Console: show debug in dev, info in prod
    console: withFilter(getConsoleSink(), isDev ? "debug" : "info"),
    // Sentry: always info and above
    sentry: withFilter(getSentrySink(), "info"),
  },
  loggers: [
    {
      category: "my-app",
      lowestLevel: "debug",
      sinks: ["console", "sentry"],
    },
  ],
});
```

[LogTape filters](https://logtape.org/manual/filters)

### 5. Log structured objects instead of strings

The core change from `console.log` to production-ready logging is switching from strings to structured objects. Instead of logging a message like `"User checked out"`, log an object packed with useful data — user IDs, cart totals, feature flags, coupon codes. This makes your logs queryable and useful for debugging. When you combine structured logs with context (set up via `withContext` on the server, or the React provider shown in the next steps), shared fields like `userId` appear automatically on every log — you only need to add what is unique to each event.

```javascript
import { getLogger } from "@logtape/logtape";

// Use nested categories to scope logs
const logger = getLogger(["my-app", "checkout"]);

// Instead of: console.log("User checked out")
// Do this:
logger.info("Checkout completed", {
  orderId: "ord_5521",
  userId: "user_987",
  cartTotal: 125.50,
  discountCode: "SAVE20",
  itemCount: 3,
  paymentMethod: "credit_card",
});
```

[LogTape structured logging](https://logtape.org/manual/struct)

### 6. Create a React logger context provider

Browsers do not have `AsyncLocalStorage`, so implicit context is not available on the frontend. Instead, create a React context provider in `lib/logger-context.tsx` that wraps your app and automatically attaches session data to every log. The provider exposes two hooks: `useLogger()` returns a context-enriched logger, and `useChildLogger()` returns a scoped child logger for a specific component or feature area — so components inherit user context without any manual wiring.

```javascript
"use client";
import { createContext, useContext, useMemo } from "react";
import { getLogger, type Logger } from "@logtape/logtape";
import { useSession } from "@/lib/auth-client"; // replace with your auth hook

interface LoggerContextValue { logger: Logger; }
const LoggerContext = createContext<LoggerContextValue | null>(null);

export function LoggerProvider({ children }: { children: React.ReactNode }) {
  const { data: session } = useSession();
  const baseLogger = getLogger(["my-app", "app"]);

  const logger = useMemo(() => {
    if (session?.user) return baseLogger.with({ user: session.user });
    return baseLogger.with({ user: { id: "anonymous" } });
  }, [session?.user, baseLogger]);

  return (
    <LoggerContext.Provider value={{ logger }}>
      {children}
    </LoggerContext.Provider>
  );
}

export function useLogger(): Logger {
  const context = useContext(LoggerContext);
  if (!context) return getLogger(["my-app", "app"]);
  return context.logger;
}

export function useChildLogger(category: string): Logger {
  const parentLogger = useLogger();
  return useMemo(() => parentLogger.getChild(category), [parentLogger, category]);
}
```

[Full React logger provider implementation](https://blog.sentry.io/trace-connected-structured-logging-with-logtape-and-sentry/)

### 7. Wrap your layout and use the logger in components

Wrap your root layout with `LoggerProvider` so every client component has access to a context-enriched logger. Then in any client component, call `useLogger()` to get a logger that already carries the session user data set up in the provider — no need to pass context manually or repeat `userId` in every log call.

```javascript
// app/layout.tsx
import { LoggerProvider } from "@/lib/logger-context";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <LoggerProvider>{children}</LoggerProvider>
      </body>
    </html>
  );
}

// Any client component
"use client";
import { useLogger } from "@/lib/logger-context";

export function PostItem({ post }: { post: Post }) {
  const logger = useLogger();

  const handleDelete = async () => {
    try {
      await deletePost(post.id);
      logger.info("Post deleted", { postId: post.id });
    } catch (err) {
      logger.error("Delete failed", { postId: post.id, error: err });
    }
  };

  // ...
}
```

[Trace-connected logging with LogTape](https://blog.sentry.io/trace-connected-structured-logging-with-logtape-and-sentry/)

### 8. View trace-connected logs in Sentry

When an error occurs, open the issue in [Issues](https://sentry.io/orgredirect/organizations/:orgslug/issues/) and scroll to the associated trace. Because Sentry connects logs to traces, every log emitted during that request is visible alongside the error, spans, and Session Replay. You can see the user ID, cart contents, coupon codes, and anything else you included as structured data.

[Sentry Logs documentation](https://docs.sentry.io/product/logs/)

### 9. Query and filter logs in the Log Explorer

Go to [Explore > Logs](https://sentry.io/orgredirect/organizations/:orgslug/explore/logs/) to search across all of your structured logs. You can filter by any attribute you included — category, severity, user ID, order ID, coupon code, or any custom field. Use LogTape categories to scope queries (e.g., `category:my-app.checkout` for checkout logs only, or `category:my-app` for everything).

```text
# Show checkout events where cart total exceeded $100
category:my-app.checkout cartTotal:>100

# Show failed checkouts using a specific coupon code
category:my-app.checkout severity:error discountCode:SAVE20

# Show all logs for a specific user
userId:user_987
```

[Log Explorer documentation](https://docs.sentry.io/product/logs/)

### 10. Create alerts and dashboards from log queries

Once you have queries that surface important patterns, turn them into alerts. For example, create an alert when a specific payment method starts failing frequently, or when error-level logs from the checkout category spike. You can also build dashboards to monitor trends over time. Configure alerts to notify your team on Slack so you can investigate before things get worse.

[Sentry Alerts documentation](https://docs.sentry.io/product/monitors-and-alerts/alerts/)

## Summary

**Your logs are structured.**

Every log carries structured attributes you can filter and search in the Logs Explorer. When something breaks, the context to understand why is already there.

## Pro Tips

- Log at milestones, not every line. Emit fewer logs packed with rich context rather than many thin logs that are hard to correlate.

- Use nested LogTape categories like `['my-app', 'api', 'checkout']` to organize logs by domain. You can then query at any level of the hierarchy.

- On the backend, use `withContext` to attach a `requestId` (e.g., `randomUUID()`) to every log in a request. This makes it easy to find all logs for a single request.

- Include high-cardinality data like user IDs, order IDs, and feature flags in your structured logs. Modern tools like Sentry handle high-cardinality data well.

## Common Pitfalls

- Don't log sensitive data like passwords, tokens, or personally identifiable information (PII). Use Sentry's data scrubbing or redact before logging.

- Always initialize Sentry before configuring LogTape. The Sentry sink needs an active Sentry client to work.

- Avoid setting your Sentry sink to `debug` level in production — it creates noise and increases costs. Use `info` or above for Sentry and keep `debug` for the console in development.

- Don't treat structured logging as a replacement for error monitoring. Use `logger.error` for expected error states, but let Sentry's error monitoring catch unhandled exceptions automatically.

## FAQ

**Do I have to use LogTape, or can I use Sentry's built-in logging?**

You can use Sentry's built-in `Sentry.logger` API directly without LogTape. LogTape adds features like hierarchical categories, implicit context propagation via AsyncLocalStorage, and the ability to send logs to multiple destinations (console, Sentry, files) from a single logger. If you need those features, LogTape is a good fit. If you just need basic structured logging sent to Sentry, the built-in API works fine.

**Does LogTape work outside of Next.js?**

Yes. LogTape works across Node.js, Deno, Bun, browsers, and edge functions. The examples here use Next.js, but the same LogTape configuration and Sentry sink work with any JavaScript framework or runtime.

**What's the difference between LogTape categories and Sentry projects?**

Sentry projects typically represent a service or application (e.g., your frontend, your API). LogTape categories are a finer-grained way to organize logs within a project — for example, `['my-app', 'api', 'checkout']` vs `['my-app', 'api', 'auth']`. You can query by category in Sentry's Log Explorer to scope your search.

**How does trace-connected logging work?**

When you use the Sentry SDK with tracing enabled, every request gets a unique trace ID. The LogTape Sentry sink automatically attaches that trace ID to each log. When you open a trace or issue in Sentry, you see all related logs, errors, and replays connected by that ID.

**How much does logging add to my Sentry bill?**

Logs are billed separately from errors and transactions. You can control costs by filtering log levels (e.g., only sending info and above to Sentry) and by logging at milestones rather than every function call. Check Sentry's pricing page for current rates.

## Next Steps

- [Read the LogTape + Sentry blog post](https://blog.sentry.io/trace-connected-structured-logging-with-logtape-and-sentry/): Get the full walkthrough of configuring LogTape with Sentry in a Next.js app, including the React Context provider pattern for frontend logging.

- [Explore Session Replay](https://sentry.io/product/session-replay/): See exactly what your users experienced leading up to an error with video-like reproductions of their sessions.

- [Set up custom dashboards](https://docs.sentry.io/product/dashboards/custom-dashboards/): Build dashboards from your structured log data to monitor trends and catch problems before your users report them.

---

*Source: [sentry.io/cookbook/structured-logging-logtape/](https://sentry.io/cookbook/structured-logging-logtape/)*
