- name
- logtape
- description
- Use this skill when writing any code that uses LogTape for logging in JavaScript or TypeScript. Covers getting loggers, the structured message syntax, configuration, library author rules, context, lazy evaluation, testing, and common mistakes to avoid. Trigger whenever the user is adding logging to a project, debugging log output, or integrating LogTape with a framework.
- license
- MIT
LogTape skill for AI coding assistants
======================================
LogTape is a zero-dependency, library-first logging framework for JavaScript
and TypeScript that works across Deno, Node.js, Bun, browsers, and edge
functions.
Full documentation: <https://logtape.org/>
Getting a logger
----------------
Always use `getLogger()` with an **array** category to enable hierarchical
filtering. The hierarchy works like a path: a parent category's configuration
applies to all children.
~~~~ typescript
import { getLogger } from "@logtape/logtape";
// Good: array form enables hierarchical filtering
const logger = getLogger(["my-app", "users", "auth"]);
// Acceptable shorthand for a single-segment category
const rootLogger = getLogger("my-app");
~~~~
Choose category segments that reflect your module structure so that operators
can selectively enable/disable logging per subsystem.
Use `logger.getChild("sub")` to derive a child logger without repeating the
full category:
~~~~ typescript
const dbLogger = logger.getChild("database");
// category = ["my-app", "users", "auth", "database"]
~~~~
See <https://logtape.org/manual/categories.md> for details.
Structured messages
-------------------
Use **named placeholders** with a properties object. This keeps messages
parseable and properties searchable:
~~~~ typescript
// Correct: structured message with named placeholders
logger.info("User {userId} logged in from {ip}", { userId, ip });
// Correct: structured data without a message
logger.info({ userId, ip, action: "login" });
// Nested property access (since 1.2.0)
logger.info("Name: {user.name}", { user: { name: "Alice" } });
~~~~
Template literal syntax is available for quick debug logging but does **not**
produce structured data:
~~~~ typescript
// Template literal: convenient but not structured
logger.debug`User ${userId} logged in`;
~~~~
See <https://logtape.org/manual/struct.md> for full structured logging details.
Severity levels
---------------
LogTape provides six levels, from most to least verbose:
| Level | Use for |
| --------- | ----------------------------------------------------- |
| `trace` | Very fine-grained diagnostic output |
| `debug` | Developer-facing diagnostic messages |
| `info` | Normal operational events (startup, shutdown, etc.) |
| `warning` | Unexpected but recoverable situations |
| `error` | Errors that affect a single operation |
| `fatal` | Unrecoverable errors that require process termination |
Use the lowest appropriate level. Reserve `error`/`fatal` for actual failures;
avoid using them for expected conditions like validation errors.
See <https://logtape.org/manual/levels.md> for details.
Configuration
-------------
### Async configuration (most common)
`configure()` is **application-only**. It must be `await`ed and called
**exactly once** at startup (e.g., in your entry point):
~~~~ typescript
import { configure, getConsoleSink } from "@logtape/logtape";
await configure({
sinks: {
console: getConsoleSink(),
},
loggers: [
{
category: "my-app",
lowestLevel: "debug",
sinks: ["console"],
},
],
});
~~~~
### Synchronous configuration
Use `configureSync()` when you cannot use `await` (e.g., top-level in CommonJS,
or in a synchronous startup path):
~~~~ typescript
import { configureSync, getConsoleSink } from "@logtape/logtape";
configureSync({
sinks: {
console: getConsoleSink(),
},
loggers: [
{
category: "my-app",
lowestLevel: "debug",
sinks: ["console"],
},
],
});
~~~~
> **Limitation:** `configureSync()` cannot use `AsyncDisposable` sinks such as
> `getStreamSink()` or sinks created with `fromAsyncSink()`.
### Key rules
- `configure()` returns a `Promise`; always `await` it.
- `configureSync()` returns `void`; do not `await` it.
- Call either one **once**. Calling again without resetting first throws
`ConfigError`.
- Do **not** mix async and sync: if you used `configure()`, reset with
`await reset()`; if you used `configureSync()`, reset with `resetSync()`.
- For tests, call `await reset()` (or `resetSync()`) in teardown.
### Framework-specific patterns
- **React**: configure **before** `createRoot()`.
- **Vue**: configure **before** `app.mount()`.
- **Next.js**: use *instrumentation.js* (server) or
*instrumentation-client.js* (client).
- **SvelteKit**: configure in *hooks.server.ts*.
See <https://logtape.org/manual/config.md> for all options.
Library author rule
-------------------
**Never call `configure()` or `configureSync()` in library code.** Libraries
should only call `getLogger()` and log messages. The application that depends
on your library decides how (or whether) to configure sinks and levels.
~~~~ typescript
// my-lib/src/client.ts — library code
import { getLogger } from "@logtape/logtape";
// Good: just get a logger, don't configure
const logger = getLogger(["my-lib", "client"]);
export function fetchData(url: string) {
logger.debug("Fetching {url}", { url });
// ...
}
~~~~
If your library wraps other LogTape-using libraries, use `withCategoryPrefix()`
to nest their logs under your category:
~~~~ typescript
import { withCategoryPrefix } from "@logtape/logtape";
export function myOperation() {
return withCategoryPrefix(["my-lib"], () => {
// Logs from inner libraries appear as ["my-lib", ...their-category]
innerLib.doWork();
});
}
~~~~
> **Note:** `withCategoryPrefix()` requires `contextLocalStorage` to be
> configured by the application.
See <https://logtape.org/manual/library.md> for the full guide.
Context with `with()` and lazy evaluation
-----------------------------------------
### Adding explicit context
Use `logger.with()` to create a child logger that attaches properties to every
subsequent log call:
~~~~ typescript
const reqLogger = logger.with({ requestId, userId });
reqLogger.info("Processing order {orderId}", { orderId });
// Log record will contain requestId, userId, AND orderId
~~~~
### Implicit context (request tracing)
Use `withContext()` to propagate context across an entire call stack without
threading loggers manually. Requires `contextLocalStorage` in configuration:
~~~~ typescript
import { configure, getConsoleSink, withContext } from "@logtape/logtape";
import { AsyncLocalStorage } from "node:async_hooks";
await configure({
sinks: { console: getConsoleSink() },
loggers: [{ category: "app", sinks: ["console"] }],
contextLocalStorage: new AsyncLocalStorage(),
});
function handleRequest(req: Request) {
withContext({ requestId: crypto.randomUUID() }, () => {
// All logs inside this callback automatically include requestId
processRequest(req);
});
}
~~~~
> **Note:** Implicit contexts are not available in browsers yet.
### Lazy evaluation
Wrap expensive computations with `lazy()` so they only run when the level is
enabled:
~~~~ typescript
import { getLogger, lazy } from "@logtape/logtape";
const logger = getLogger(["my-app"]);
logger.debug("System state: {state}", {
state: lazy(() => JSON.stringify(getExpensiveState())),
});
~~~~
For structured data, pass a callback as the second argument:
~~~~ typescript
logger.debug("Diagnostics", () => ({
heap: process.memoryUsage().heapUsed,
uptime: process.uptime(),
}));
~~~~
For **async** lazy evaluation, pass an async callback and `await` the result:
~~~~ typescript
await logger.info("User details", async () => ({
user: await fetchUserDetails(),
}));
~~~~
For multiple expensive log calls, use `isEnabledFor()`:
~~~~ typescript
if (logger.isEnabledFor("debug")) {
const snapshot = await captureExpensiveSnapshot();
logger.debug("Snapshot: {data}", { data: snapshot });
}
~~~~
See <https://logtape.org/manual/lazy.md> and
<https://logtape.org/manual/contexts.md> for details.
Logging errors
--------------
Pass `Error` objects directly to `error()` or `fatal()`. You can attach
extra properties as a second argument:
~~~~ typescript
try {
await riskyOperation();
} catch (error) {
logger.error(error, { operation: "riskyOperation", userId });
}
~~~~
Sinks and formatters
--------------------
### Built-in sinks
~~~~ typescript
import {
configure,
getConsoleSink,
getStreamSink,
} from "@logtape/logtape";
await configure({
sinks: {
console: getConsoleSink(),
stderr: getStreamSink(Writable.toWeb(process.stderr)),
},
loggers: [{ category: "app", sinks: ["console"] }],
});
~~~~
### Sink filters
Use `withFilter()` to route different levels to different sinks:
~~~~ typescript
import { configure, getConsoleSink, withFilter } from "@logtape/logtape";
await configure({
sinks: {
errorsOnly: withFilter(getConsoleSink(), "error"),
allLevels: getConsoleSink(),
},
loggers: [
{ category: "app", sinks: ["allLevels", "errorsOnly"] },
],
});
~~~~
### Formatters
~~~~ typescript
import {
configure,
getAnsiColorFormatter,
getConsoleSink,
getJsonLinesFormatter,
getLogfmtFormatter,
} from "@logtape/logtape";
// Pretty ANSI-colored output for development
getConsoleSink({ formatter: getAnsiColorFormatter() });
// JSON Lines for production / log aggregation
getConsoleSink({ formatter: getJsonLinesFormatter() });
// logfmt for readable structured logs
getConsoleSink({ formatter: getLogfmtFormatter() });
~~~~
For even nicer development output, use `@logtape/pretty`:
~~~~ typescript
import { getPrettyFormatter } from "@logtape/pretty";
getConsoleSink({ formatter: getPrettyFormatter() });
~~~~
### Disposal
Non-blocking sinks and stream sinks hold resources. In **edge functions** or
short-lived processes, explicitly dispose before exit:
~~~~ typescript
View on GitHub