diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 64b6e46..81220e9 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -112,10 +112,14 @@ jobs: set -ex bun install if [[ "$GITHUB_REF_TYPE" = "tag" ]]; then + bun add -D "@logtape/logtape@$GITHUB_REF_NAME" + bun add -D @logtape/otel@latest EXTRA_NAV_TEXT=Unstable \ EXTRA_NAV_LINK="$UNSTABLE_DOCS_URL" \ bun run build else + bun add -D @logtape/logtape@dev + bun add -D @logtape/otel@dev EXTRA_NAV_TEXT=Stable \ EXTRA_NAV_LINK="$STABLE_DOCS_URL" \ bun run build diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 6f361aa..34ac59b 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,6 +1,7 @@ import { Presets, SingleBar } from "cli-progress"; import { jsrRef } from "markdown-it-jsr-ref"; import { defineConfig } from "vitepress"; +import { transformerTwoslash } from "@shikijs/vitepress-twoslash"; const progress = new SingleBar({}, Presets.shades_classic); let started = false; @@ -110,6 +111,21 @@ export default defineConfig({ }, head: plausibleScript, markdown: { + codeTransformers: [ + transformerTwoslash({ + twoslashOptions: { + compilerOptions: { + lib: ["dom", "dom.iterable", "esnext"], + types: [ + "dom", + "dom.iterable", + "esnext", + "@teidesu/deno-types/full", + ], + }, + }, + }), + ], config(md) { md.use(jsrRefPlugin); }, diff --git a/docs/.vitepress/theme/index.mts b/docs/.vitepress/theme/index.mts index 508d8b4..8717fa2 100644 --- a/docs/.vitepress/theme/index.mts +++ b/docs/.vitepress/theme/index.mts @@ -1,4 +1,13 @@ -import DefaultTheme from 'vitepress/theme' +import TwoslashFloatingVue from "@shikijs/vitepress-twoslash/client"; +import type { EnhanceAppContext } from "vitepress"; +import Theme from "vitepress/theme"; + +import "@shikijs/vitepress-twoslash/style.css"; import './custom.css' -export default DefaultTheme \ No newline at end of file +export default { + extends: Theme, + enhanceApp({ app }: EnhanceAppContext) { + app.use(TwoslashFloatingVue); + }, +}; diff --git a/docs/bun.lockb b/docs/bun.lockb index 1d02db9..41b90ba 100755 Binary files a/docs/bun.lockb and b/docs/bun.lockb differ diff --git a/docs/manual/categories.md b/docs/manual/categories.md index d003b44..36b0ea0 100644 --- a/docs/manual/categories.md +++ b/docs/manual/categories.md @@ -14,7 +14,7 @@ the log level of loggers at different levels of the category hierarchy. Here's an example of setting log levels for different categories: -~~~~ typescript{9-10} +~~~~ typescript{9-10} twoslash import { configure, getConsoleSink } from "@logtape/logtape"; await configure({ @@ -35,7 +35,9 @@ Child loggers You can get a child logger from a parent logger by calling `~Logger.getChild()`: -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +// ---cut-before--- const logger = getLogger(["my-app"]); const childLogger = logger.getChild("my-module"); // equivalent: const childLogger = getLogger(["my-app", "my-module"]); @@ -43,7 +45,9 @@ const childLogger = logger.getChild("my-module"); The `~Logger.getChild()` method can take an array of strings as well: -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +// ---cut-before--- const logger = getLogger(["my-app"]); const childLogger = logger.getChild(["my-module", "foo"]); // equivalent: const childLogger = getLogger(["my-app", "my-module", "foo"]); @@ -75,7 +79,17 @@ to an empty array. > if there's an issue with your main sink, you can still receive meta logs about > the issue: > -> ~~~~ typescript +> ~~~~ typescript twoslash +> // @noErrors: 2307 +> import { type Sink } from "@logtape/logtape"; +> /** +> * A hypothetical function to get your main sink. +> * @returns The main sink. +> */ +> function getYourMainSink(): Sink { +> return 0 as unknown as Sink; +> } +> // ---cut-before--- > import { configure, getConsoleSink } from "@logtape/logtape"; > import { getYourMainSink } from "./your-main-sink.ts"; > diff --git a/docs/manual/config.md b/docs/manual/config.md index 63cf28c..131c0d2 100644 --- a/docs/manual/config.md +++ b/docs/manual/config.md @@ -19,7 +19,7 @@ At its core, configuring LogTape involves three main components: Here's a simple configuration to get you started: -~~~~ typescript +~~~~ typescript twoslash import { configure, getConsoleSink } from "@logtape/logtape"; await configure({ @@ -53,7 +53,8 @@ Crafting your configuration [Sinks](./sinks.md) determine where your logs end up. You can have multiple sinks for different purposes: -~~~~ typescript +~~~~ typescript twoslash +// @noErrors: 2345 import { configure, getConsoleSink, getFileSink } from "@logtape/logtape"; await configure({ @@ -71,7 +72,10 @@ await configure({ [Filters](./filters.md) allow you to fine-tune which logs are processed. They can be based on log levels, content, or custom logic: -~~~~ typescript +~~~~ typescript twoslash +// @noErrors: 2345 +import { configure } from "@logtape/logtape"; +// ---cut-before--- await configure({ // ... sinks configuration filters: { @@ -82,7 +86,9 @@ await configure({ return record.level === "error" || record.level === "fatal"; }, containsUserData(record) { - return record.message.some(part => part.includes("user")); + return record.message.some( + part => typeof part === "string" && part.includes("user") + ); }, }, // ... loggers configuration @@ -94,7 +100,10 @@ await configure({ Loggers are where you bring everything together. You can set up different loggers for different parts of your application: -~~~~ typescript +~~~~ typescript twoslash +// @noErrors: 2345 +import { configure } from "@logtape/logtape"; +// ---cut-before--- await configure({ // ... sinks and filters configuration loggers: [ @@ -139,7 +148,9 @@ Here's how you might do that: ::: code-group -~~~~ typescript{1,6,11-12} [Deno] +~~~~ typescript{1,6,11-12} twoslash [Deno] +import { configure, getConsoleSink, getFileSink } from "@logtape/logtape"; +// ---cut-before--- const isDevelopment = Deno.env.get("DENO_DEPLOYMENT_ID") == null; await configure({ @@ -157,7 +168,11 @@ await configure({ }); ~~~~ -~~~~ typescript{1,6,11-12} [Node] +~~~~ typescript{1,6,11-12} twoslash [Node.js] +import "@types/node"; +import process from "node:process"; +import { configure, getConsoleSink, getFileSink } from "@logtape/logtape"; +// ---cut-before--- const isDevelopment = process.env.NODE_ENV === "development"; await configure({ @@ -183,7 +198,10 @@ Remember that calling `configure()` with `reset: true` option will reset any existing configuration. If you need to change the configuration at runtime, you can call `configure()` again with `reset: true` and the new settings: -~~~~ typescript +~~~~ typescript twoslash +import { type Config, configure } from "@logtape/logtape"; +const initialConfig = {} as unknown as Config; +// ---cut-before--- // Initial configuration await configure(initialConfig); @@ -204,7 +222,10 @@ await configure({ Or you can explicitly call `reset()` to clear the existing configuration: -~~~~ typescript +~~~~ typescript twoslash +import { type Config } from "@logtape/logtape"; +const initialConfig = {} as unknown as Config; +// ---cut-before--- import { configure, reset } from "@logtape/logtape"; await configure(initialConfig); diff --git a/docs/manual/contexts.md b/docs/manual/contexts.md index abf433f..82508b3 100644 --- a/docs/manual/contexts.md +++ b/docs/manual/contexts.md @@ -8,7 +8,9 @@ messages. A context is a key-value map. You can set a context for a logger and log messages `~Logger.with()` the context. Here's an example of setting a context for a logger: -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +// ---cut-before--- const logger = getLogger(["my-app", "my-module"]); const ctx = logger.with({ userId: 1234, requestId: "abc" }); ctx.info `This log message will have the context (userId & requestId).`; @@ -18,10 +20,12 @@ ctx.warn("Context can be used inside message template: {userId}, {requestId}."); The context is inherited by child loggers. Here's an example of setting a context for a parent logger and logging messages with a child logger: -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +// ---cut-before--- const logger = getLogger(["my-app"]); const parentCtx = logger.with({ userId: 1234, requestId: "abc" }); -const childCtx = parentCtx.getLogger(["my-module"]); +const childCtx = parentCtx.getChild(["my-module"]); childCtx.debug("This log message will have the context: {userId} {requestId}."); ~~~~ diff --git a/docs/manual/filters.md b/docs/manual/filters.md index 2143a52..3d78152 100644 --- a/docs/manual/filters.md +++ b/docs/manual/filters.md @@ -6,7 +6,9 @@ and returns a boolean value. If the filter returns `true`, the log record is passed to the sinks; otherwise, the log record is discarded. The signature of `Filter` is: -~~~~ typescript +~~~~ typescript twoslash +import type { LogRecord } from "@logtape/logtape"; +// ---cut-before--- export type Filter = (record: LogRecord) => boolean; ~~~~ @@ -17,14 +19,17 @@ the `~Config.loggers` object to assign filters to loggers. For example, the following filter discards log messages whose property `elapsed` is less than 100 milliseconds: -~~~~ typescript {5-9} +~~~~ typescript{5-9} twoslash +// @noErrors: 2345 import { configure, type LogRecord } from "@logtape/logtape"; await configure({ // Omitted for brevity filters: { tooSlow(record: LogRecord) { - return "elapsed" in record.properties && record.properties.elapsed >= 100; + return "elapsed" in record.properties + && typeof record.properties.elapsed === "number" + && record.properties.elapsed >= 100; }, }, loggers: [ @@ -47,12 +52,13 @@ filter log messages by their log levels. The level filter factory takes a `LogLevel` string and returns a level filter. For example, the following level filter discards log messages whose log level is less than `info`: -~~~~ typescript -import { getLevelFilter } from "@logtape/logtape"; +~~~~ typescript twoslash +// @noErrors: 2345 +import { configure, getLevelFilter } from "@logtape/logtape"; await configure({ filters: { - infoOrHigher: getLevelFilter("info"); // [!code highlight] + infoOrHigher: getLevelFilter("info"), // [!code highlight] }, // Omitted for brevity }); @@ -65,14 +71,17 @@ Sink filter A sink filter is a filter that is applied to a specific [sink](./sinks.md). You can add a sink filter to a sink by decorating the sink with `withFilter()`: -~~~~ typescript {5-8} -import { getConsoleSink, withFilter } from "@logtape/logtape"; +~~~~ typescript{5-8} twoslash +// @noErrors: 2345 +import { configure, getConsoleSink, withFilter } from "@logtape/logtape"; await configure({ sinks: { filteredConsole: withFilter( getConsoleSink(), - log => "elapsed" in log.properties && log.properties.elapsed >= 100, + log => "elapsed" in log.properties && + typeof log.properties.elapsed === "number" && + log.properties.elapsed >= 100, ), }, // Omitted for brevity diff --git a/docs/manual/formatters.md b/docs/manual/formatters.md index 38dbb3f..3013097 100644 --- a/docs/manual/formatters.md +++ b/docs/manual/formatters.md @@ -241,7 +241,9 @@ Fully customized text formatter A text formatter is just a function that takes a log record and returns a string. The type of a text formatter is `TextFormatter`: -~~~~ typescript +~~~~ typescript twoslash +import type { LogRecord } from "@logtape/logtape"; +// ---cut-before--- export type TextFormatter = (record: LogRecord) => string; ~~~~ @@ -250,7 +252,9 @@ a function that takes a log record and returns a string. For example, the following function is a text formatter that formats log records into [JSON Lines]: -~~~~ typescript +~~~~ typescript twoslash +import type { LogRecord } from "@logtape/logtape"; +// ---cut-before--- function jsonLinesFormatter(record: LogRecord): string { return JSON.stringify(record) + "\n"; } diff --git a/docs/manual/levels.md b/docs/manual/levels.md index 7351b02..a0697c6 100644 --- a/docs/manual/levels.md +++ b/docs/manual/levels.md @@ -1,7 +1,11 @@ Severity levels =============== -When you're logging events in your application, not all messages are created equal. Some might be routine information, while others could be critical errors that need immediate attention. That's where severity levels come in. LogTape provides five severity levels to help you categorize your log messages effectively. +When you're logging events in your application, not all messages are created +equal. Some might be routine information, while others could be critical errors +that need immediate attention. That's where severity levels come in. +LogTape provides five severity levels to help you categorize your log messages +effectively. Five severity levels @@ -25,9 +29,14 @@ Let's break down when you might use each of these: ### Debug -Use this level for detailed information that's mostly useful when diagnosing problems. Debug logs are typically not shown in production environments. +Use this level for detailed information that's mostly useful when diagnosing +problems. Debug logs are typically not shown in production environments. -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +const logger = getLogger(); +const elapsedMs = 0 as number; +// ---cut-before--- logger.debug("Database query took {elapsedMs}ms to execute.", { elapsedMs }); ~~~~ @@ -35,7 +44,11 @@ logger.debug("Database query took {elapsedMs}ms to execute.", { elapsedMs }); This level is for general information about the application's operation. -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +const logger = getLogger(); +const username = "" as string; +// ---cut-before--- logger.info("User {username} logged in successfully.", { username }); ~~~~ @@ -45,7 +58,10 @@ Use this when something unexpected happened, but the application can continue functioning. This level is often used for events that are close to causing errors. -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +const logger = getLogger(); +// ---cut-before--- logger.warn("API rate limit is close to exceeding, 95% of limit reached."); ~~~~ @@ -55,7 +71,11 @@ This level indicates a significant problem that prevented a specific operation from being completed. Use this for errors that need attention but don't necessarily cause the application to stop. -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +const logger = getLogger(); +const err = new Error(); +// ---cut-before--- logger.error( "Failed to save user data to database.", { userId: "12345", error: err }, @@ -67,7 +87,11 @@ logger.error( Use this for critical errors that cause the application to abort. Fatal errors are typically unrecoverable and require immediate attention. -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +const logger = getLogger(); +const error = new Error(); +// ---cut-before--- logger.fatal("Unrecoverable error: Database connection lost.", { error }); ~~~~ @@ -88,9 +112,13 @@ When deciding which level to use, consider: Configuring severity levels --------------------------- -You can control which severity levels are logged in different parts of your application. For example: +You can control which severity levels are logged in different parts of your +application. For example: -~~~~ typescript +~~~~ typescript twoslash +// @noErrors: 2345 +import { configure } from "@logtape/logtape"; +// ---cut-before--- await configure({ // ... other configuration ... loggers: [ @@ -121,4 +149,6 @@ Best practices 4. *Consider performance*: Remember that logging, especially at lower levels, can impact performance in high-volume scenarios. -By using severity levels effectively, you can create logs that are informative, actionable, and easy to navigate. This will make debugging and monitoring your application much more manageable. +By using severity levels effectively, you can create logs that are informative, +actionable, and easy to navigate. This will make debugging and monitoring your +application much more manageable. diff --git a/docs/manual/library.md b/docs/manual/library.md index 678a1c3..d3ac878 100644 --- a/docs/manual/library.md +++ b/docs/manual/library.md @@ -19,7 +19,7 @@ this seamlessly. 1. *Use namespaced categories*: Start your log categories with your library name to avoid conflicts. - ~~~~ typescript + ~~~~ typescript twoslash import { getLogger } from "@logtape/logtape"; const logger = getLogger(["my-awesome-lib", "database"]); @@ -36,7 +36,13 @@ this seamlessly. 4. *Provide context*: Use [structured logging](./struct.md) to provide relevant context with each log message. - ~~~~ typescript + ~~~~ typescript twoslash + import { getLogger } from "@logtape/logtape"; + const logger = getLogger(["my-awesome-lib", "database"]); + const dbHost: string = ""; + const dbPort: number = 0; + const dbUser: string = ""; + // ---cut-before--- logger.info("Database connection established", { host: dbHost, port: dbPort, @@ -48,7 +54,7 @@ this seamlessly. Here's an example of how you might use LogTape in a library: -```typescript +~~~~ typescript twoslash // my-awesome-lib/database.ts import { getLogger } from "@logtape/logtape"; @@ -87,7 +93,7 @@ export class Database { // Query logic here } } -``` +~~~~ For application developers @@ -112,7 +118,8 @@ you have full control over how logs from that library are handled. Here's how you might configure logging for the example library we created above: -~~~~ typescript +~~~~ typescript twoslash +// @noErrors: 2307 import { configure, getConsoleSink, getFileSink } from "@logtape/logtape"; import { Database } from "my-awesome-lib"; diff --git a/docs/manual/sinks.md b/docs/manual/sinks.md index 5c628a1..51668f5 100644 --- a/docs/manual/sinks.md +++ b/docs/manual/sinks.md @@ -5,13 +5,16 @@ A sink is a destination of log messages. LogTape currently provides a few sinks: console and stream. However, you can easily add your own sinks. The signature of a `Sink` is: -~~~~ typescript +~~~~ typescript twoslash +import type { LogRecord } from "@logtape/logtape"; +// ---cut-before--- export type Sink = (record: LogRecord) => void; ~~~~ Here's a simple example of a sink that writes log messages to console: -~~~~ typescript {5-7} +~~~~ typescript{5-7} twoslash +// @noErrors: 2345 import { configure } from "@logtape/logtape"; await configure({ @@ -30,7 +33,8 @@ Console sink Of course, you don't have to implement your own console sink because LogTape provides a console sink: -~~~~ typescript +~~~~ typescript twoslash +// @noErrors: 2345 import { configure, getConsoleSink } from "@logtape/logtape"; await configure({ @@ -54,7 +58,10 @@ messages to the standard error: ::: code-group -~~~~ typescript [Deno] +~~~~ typescript twoslash [Deno] +// @noErrors: 2345 +import { configure, getStreamSink } from "@logtape/logtape"; +// ---cut-before--- await configure({ sinks: { stream: getStreamSink(Deno.stderr.writable), // [!code highlight] @@ -63,7 +70,11 @@ await configure({ }); ~~~~ -~~~~ typescript {5} [Node.js] +~~~~ typescript{5} twoslash [Node.js] +// @noErrors: 2345 +import "@types/node"; +import { configure, getStreamSink } from "@logtape/logtape"; +// ---cut-before--- import stream from "node:stream"; await configure({ @@ -74,7 +85,12 @@ await configure({ }); ~~~~ -~~~~ typescript {1-13,17} [Bun] +~~~~ typescript{1-13,17} twoslash [Bun] +// @noErrors: 2339 2345 +import "@types/bun"; +import { FileSink } from "bun"; +import { configure, getStreamSink } from "@logtape/logtape"; +// ---cut-before--- let writer: FileSink | undefined = undefined; const stdout = new WritableStream({ start() { @@ -123,8 +139,9 @@ File sink LogTape provides a file sink as well. Here's an example of a file sink that writes log messages to a file: -~~~~ typescript -import { getFileSink } from "@logtape/logtape"; +~~~~ typescript twoslash +// @noErrors: 2345 +import { configure, getFileSink } from "@logtape/logtape"; await configure({ sinks: { @@ -167,13 +184,14 @@ which can cause issues with file handling, log analysis, and storage management. To use the rotating file sink, you can use the `getRotatingFileSink()` function. Here's an example of a rotating file sink that writes log messages to a file: -~~~~ typescript -import { getRotatingFileSink } from "@logtape/logtape"; +~~~~ typescript twoslash +// @noErrors: 2345 +import { configure, getRotatingFileSink } from "@logtape/logtape"; await configure({ sinks: { file: getRotatingFileSink("my-app.log", { - maxFileSize: 1024 * 1024, // 1 MiB + maxSize: 0x400 * 0x400, // 1 MiB maxFiles: 5, }), }, @@ -202,7 +220,8 @@ You can customize the format by providing a text formatter. Here's an example of colorizing log messages in your terminal using the `ansiColorFormatter`: -~~~~ typescript +~~~~ typescript twoslash +// @noErrors: 2345 import { ansiColorFormatter, configure, @@ -259,7 +278,7 @@ bun add @logtape/otel The quickest way to get started is to use the [`getOpenTelemetrySink()`] function without any arguments: -~~~~ typescript +~~~~ typescript twoslash import { configure } from "@logtape/logtape"; import { getOpenTelemetrySink } from "@logtape/otel"; @@ -297,7 +316,9 @@ disposed of when the configuration is reset or the program exits. The type of a disposable sink is: `Sink & Disposable`. You can create a disposable sink by defining a `[Symbol.dispose]` method: -~~~~ typescript +~~~~ typescript twoslash +import type { LogRecord, Sink } from "@logtape/logtape"; +// ---cut-before--- const disposableSink: Sink & Disposable = (record: LogRecord) => { console.log(record.message); }; @@ -310,7 +331,9 @@ A sink can be asynchronously disposed of as well. The type of an asynchronous disposable sink is: `Sink & AsyncDisposable`. You can create an asynchronous disposable sink by defining a `[Symbol.asyncDispose]` method: -~~~~ typescript +~~~~ typescript twoslash +import type { LogRecord, Sink } from "@logtape/logtape"; +// ---cut-before--- const asyncDisposableSink: Sink & AsyncDisposable = (record: LogRecord) => { console.log(record.message); }; @@ -330,7 +353,10 @@ useful when you want to flush the buffer of a sink without blocking returning a response in edge functions. Here's an example of using the `dispose()` with [`ctx.waitUntil()`] in Cloudflare Workers: -~~~~ typescript +~~~~ typescript twoslash +// @noErrors: 2345 +import { type ExportedHandler, Response } from "@cloudflare/workers-types"; +// ---cut-before--- import { configure, dispose } from "@logtape/logtape"; export default { @@ -338,8 +364,9 @@ export default { await configure({ /* ... */ }); // ... ctx.waitUntil(dispose()); + return new Response("..."); } -} +} satisfies ExportedHandler; ~~~~ [`ctx.waitUntil()`]: https://developers.cloudflare.com/workers/runtime-apis/context/#waituntil diff --git a/docs/manual/start.md b/docs/manual/start.md index 58c6190..33401b9 100644 --- a/docs/manual/start.md +++ b/docs/manual/start.md @@ -7,7 +7,7 @@ Setting up Set up LogTape in the entry point of your application using `configure()`: -~~~~ typescript +~~~~ typescript twoslash import { configure, getConsoleSink } from "@logtape/logtape"; await configure({ @@ -26,7 +26,7 @@ await configure({ And then you can use LogTape in your application or library: -~~~~ typescript +~~~~ typescript twoslash import { getLogger } from "@logtape/logtape"; const logger = getLogger(["my-app", "my-module"]); @@ -45,7 +45,11 @@ How to log There are total 5 log levels: `debug`, `info`, `warning`, `error`, `fatal` (in the order of verbosity). You can log messages with the following syntax: -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +const logger = getLogger([]); +const value = 0 as unknown; +// ---cut-before--- logger.debug `This is a debug message with ${value}.`; logger.info `This is an info message with ${value}.`; logger.warn `This is a warning message with ${value}.`; @@ -58,7 +62,11 @@ logger.fatal `This is a fatal message with ${value}.`; You can also log messages with a function call. In this case, log messages are structured data: -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +const logger = getLogger([]); +const value = 0 as unknown; +// ---cut-before--- logger.debug("This is a debug message with {value}.", { value }); logger.info("This is an info message with {value}.", { value }); logger.warn("This is a warning message with {value}.", { value }); @@ -75,7 +83,15 @@ Sometimes, values to be logged are expensive to compute. In such cases, you can use a function to defer the computation so that it is only computed when the log message is actually logged: -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +const logger = getLogger([]); +/** + * A hypothetical function that computes a value, which is expensive. + * @returns The computed value. + */ +function computeValue(): unknown { return 0; } +// ---cut-before--- logger.debug(l => l`This is a debug message with ${computeValue()}.`); logger.debug("Or you can use a function call: {value}.", () => { return { value: computeValue() }; diff --git a/docs/manual/struct.md b/docs/manual/struct.md index dd8013c..00b9844 100644 --- a/docs/manual/struct.md +++ b/docs/manual/struct.md @@ -24,9 +24,10 @@ include additional context and metadata with your log messages. Including structured data in log messages ----------------------------------------- -You can pass an object as the second argument to any log method. The properties of this object will be included as structured data in the log record. +You can pass an object as the second argument to any log method. The properties +of this object will be included as structured data in the log record: -~~~~ typescript +~~~~ typescript twoslash import { getLogger } from "@logtape/logtape"; const logger = getLogger(["my-app"]); @@ -44,7 +45,10 @@ the `userId`, `username`, and `loginTime` as structured fields. You can use placeholders in your log messages. The values for these placeholders will be included as structured data. -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +const logger = getLogger(); +// ---cut-before--- logger.info("User {username} (ID: {userId}) logged in at {loginTime}", { userId: 123456, username: "johndoe", @@ -58,7 +62,10 @@ message while still maintaining it as separate fields in the log record. > [!TIP] > The way to log single curly braces `{` is to double the brace: > -> ~~~~ typescript +> ~~~~ typescript twoslash +> import { getLogger } from "@logtape/logtape"; +> const logger = getLogger(); +> // ---cut-before--- > logger.debug("This logs {{single}} curly braces."); > ~~~~ @@ -75,7 +82,16 @@ If computing the structured data is expensive and you want to avoid unnecessary computation when the log level is not enabled, you can use a function to provide the structured data: -~~~~ typescript +~~~~ typescript twoslash +import { getLogger } from "@logtape/logtape"; +const logger = getLogger(); +const startTime = performance.now(); +/** + * A hypothetical function that computes a value, which is expensive. + * @returns The computed value. + */ +function expensiveComputation(): unknown { return 0; } +// ---cut-before--- logger.debug("Expensive operation completed", () => ({ result: expensiveComputation(), duration: performance.now() - startTime @@ -88,10 +104,12 @@ The function will only be called if the debug log level is enabled. Configuring sinks for structured logging ---------------------------------------- -To make the most of structured logging, you'll want to use sinks that can handle structured data. For example, you can output logs in [JSON Lines] format by +To make the most of structured logging, you'll want to use sinks that can handle +structured data. For example, you can output logs in [JSON Lines] format by providing a [text formatter](./formatters.md) to a file sink: -~~~~ typescript +~~~~ typescript twoslash +// @noErrors: 2345 import { configure, getFileSink } from "@logtape/logtape"; await configure({ @@ -124,7 +142,7 @@ Filtering based on structured data You can create filters that use the structured data in your log records: -```typescript +~~~~ typescript twoslash import { configure, getConsoleSink } from "@logtape/logtape"; await configure({ @@ -143,7 +161,7 @@ await configure({ } ] }); -``` +~~~~ This filter will only allow logs with a `"high"` priority or error level to pass through. diff --git a/docs/manual/testing.md b/docs/manual/testing.md index 9439548..eee379e 100644 --- a/docs/manual/testing.md +++ b/docs/manual/testing.md @@ -12,7 +12,8 @@ useful when you want to reset the configuration between tests. For example, the following code shows how to reset the configuration after a test (regardless of whether the test passes or fails) in Deno: -~~~~ typescript +~~~~ typescript twoslash +// @noErrors: 2345 import { configure, reset } from "@logtape/logtape"; Deno.test("my test", async (t) => { @@ -37,7 +38,8 @@ Buffer sink For testing purposes, you may want to collect log messages in memory. Although LogTape does not provide a built-in buffer sink, you can easily implement it: -~~~~ typescript +~~~~ typescript twoslash +// @noErrors: 2345 import { type LogRecord, configure } from "@logtape/logtape"; const buffer: LogRecord[] = []; diff --git a/docs/package.json b/docs/package.json index db9428e..fe06849 100644 --- a/docs/package.json +++ b/docs/package.json @@ -2,7 +2,13 @@ "dependencies": {}, "devDependencies": { "@biomejs/biome": "^1.8.3", - "@types/bun": "^1.1.8", + "@cloudflare/workers-types": "^4.20240909.0", + "@logtape/logtape": "^0.5.1", + "@logtape/otel": "^0.2.0", + "@shikijs/vitepress-twoslash": "^1.17.6", + "@teidesu/deno-types": "^1.46.3", + "@types/bun": "^1.1.9", + "@types/node": "^22.5.5", "cli-progress": "^3.12.0", "markdown-it-jsr-ref": "^0.3.2", "vitepress": "^1.3.4"