Replacing console.log with structured, trace-connected logging using LogTape and Sentry

Move from noisy string logs to structured, queryable, trace-connected events that help you debug production issues across your entire stack.

Features
SDKs
Category Debugging
Share
Time
20-30 minutes
Difficulty
Intermediate
Steps
10 steps

Before you start

SDKs and packages
Accounts & access
  • Sentry account 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)

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.

LogTape installation docs
# npm
npm add @logtape/logtape @logtape/sentry
# pnpm
pnpm add @logtape/logtape @logtape/sentry
# yarn
yarn add @logtape/logtape @logtape/sentry

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.

Sentry JavaScript log setup
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"],
    },
  ],
});

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.

LogTape contexts
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(),
});

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.

LogTape filters
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"],
    },
  ],
});

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.

LogTape structured logging
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",
});

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.

Full React logger provider implementation
"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]);
}

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.

Trace-connected logging with LogTape
// 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 });
    }
  };

  // ...
}

8
View trace-connected logs in Sentry

When an error occurs, open the issue in 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
Sentry issue view showing trace-connected structured logs alongside the error and spans

9
Query and filter logs in the Log Explorer

Go to 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).

Log Explorer documentation
# 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

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
Sentry alert configuration showing log-based filters by category and severity with anomaly detection thresholds

That's it.

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.

  • 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

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.

Frequently asked questions

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

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.