Skip to content

Usage ​

Lumberjack helps you build one wide structured log event around a unit of work. Start a Logging Span, add Metrics as the work progresses, then close the span once to produce a Closed Logging Span.

Use log_close() when normal console logging is enough. Use close() when you want to send the Log Payload somewhere else, such as an HTTP logging endpoint.

Install ​

Install the package from npm:

sh
npm install @luckybeard/lumberjack

Then import LoggingSpan from the package entrypoint:

ts
import { LoggingSpan } from "@luckybeard/lumberjack";

Log To Console With log_close() ​

log_close() closes the Logging Span, writes the Log Payload with console[closed.level], and returns the Closed Logging Span.

This is the simplest path when your runtime already collects console logs.

ts
import { LoggingSpan } from "@luckybeard/lumberjack";

async function submitCheckout(cartId: string) {
  const span = LoggingSpan.start("checkout.submit").add_metric("cart.id", cartId).set_level("info");

  try {
    const response = await fetch("https://payments.example.com/checkout", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ cartId }),
    });

    span
      .add_metric("payment.response", response)
      .add_metric("checkout.accepted", response.ok)
      .set_level(response.ok ? "info" : "warn");

    if (!response.ok) {
      throw new Error(`Checkout failed with ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    span.add_metric("error", error).set_level("error");
    throw error;
  } finally {
    span.log_close();
  }
}

The emitted payload includes Lumberjack's span lifecycle data plus the Metrics you added:

ts
{
  "span.name": "checkout.submit",
  "span.time_start": "2026-04-29T10:00:00.000Z",
  "cart.id": "cart_123",
  "payment.response": {
    status: 200,
    statusText: "OK",
    headers: {
      "x-request-id": "req_123"
    }
  },
  "checkout.accepted": true,
  "span.running_duration_ms": 42,
  "span.time_end": "2026-04-29T10:00:00.042Z"
}

Send Logs With close() ​

Use close() when you want the Closed Logging Span without writing to console. The app remains responsible for delivery, retries, authentication, batching, and endpoint behavior.

The recommended HTTP body is { level, data }, because the severity level is stored on the Closed Logging Span and the Log Payload is stored on closed.data.

ts
import { LoggingSpan, type IClosedLoggerSpan } from "@luckybeard/lumberjack";

async function sendLog(closed: IClosedLoggerSpan) {
  await fetch("https://logs.example.com/events", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      level: closed.level,
      data: closed.data,
    }),
  });
}

async function submitCheckout(cartId: string) {
  const span = LoggingSpan.start("checkout.submit").add_bulk_metrics({
    "cart.id": cartId,
    "checkout.channel": "web",
  });

  try {
    const response = await fetch("https://payments.example.com/checkout", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ cartId }),
    });

    span
      .add_metric("payment.response", response)
      .add_metric("checkout.accepted", response.ok)
      .set_level(response.ok ? "info" : "warn");

    if (!response.ok) {
      throw new Error(`Checkout failed with ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    span.add_metric("error", error).set_level("error");
    throw error;
  } finally {
    const closed = span.close();

    try {
      await sendLog(closed);
    } catch (deliveryError) {
      console.warn("Failed to deliver checkout log", deliveryError);
    }
  }
}

Choose one closing method per Logging Span: call either log_close() or close(), not both.

Metrics ​

Metrics are named values recorded on a Logging Span. Use dotted names to group related values:

ts
const span = LoggingSpan.start("checkout.submit")
  .add_metric("cart.id", "cart_123")
  .add_metric("customer.id", "customer_456")
  .add_metric("checkout.total", 129.99)
  .add_metric("checkout.currency", "USD");

Use add_bulk_metrics() when you already have several values at the same point in the operation:

ts
span.add_bulk_metrics({
  "cart.id": "cart_123",
  "customer.id": "customer_456",
  "checkout.channel": "web",
});

If the same key is added more than once before closing, the latest value is kept.

Metric Namespaces ​

Use create_metric_namespace() when you want shared Metric names across requests or modules. Create the helper once at module level and reuse it wherever those Metrics are recorded:

ts
import { create_metric_namespace } from "@luckybeard/lumberjack";

export const checkout_metric = create_metric_namespace("checkout");

For one Metric, call name():

ts
span.add_metric(checkout_metric.name("accepted"), true);

For several Metrics in the same namespace, call metrics() before add_bulk_metrics():

ts
span.add_bulk_metrics(
  checkout_metric.metrics({
    total: 129.99,
    currency: "USD",
  }),
);

This produces Metric names like checkout.accepted, checkout.total, and checkout.currency. The helper only creates Metric names; values are still stored, cloned, and sanitized by the Logging Span.

The span namespace is reserved for Lumberjack lifecycle Metrics such as span.name, span.time_start, span.running_duration_ms, and span.time_end.

Levels ​

Every Logging Span has a level. The default is info.

ts
span.set_level("warn");

Supported levels are:

  • info
  • warn
  • error
  • trace

log_close() uses the level to choose the console method. For example, an error level span is written with console.error(closed.data).

Closing Behavior ​

Closing a Logging Span adds lifecycle Metrics to the Log Payload:

  • span.running_duration_ms
  • span.time_end

The span also includes these Metrics from the moment it is started:

  • span.name
  • span.time_start

After a span is closed, later calls to add_metric() are ignored and Lumberjack writes a warning to console.warn.

Sanitization ​

Lumberjack clones object Metrics and sanitizes top-level keys before storing them on the Log Payload.

Keys containing token, key, password, or otp are replaced with [redacted]. Keys containing email or phone are masked.

ts
const closed = LoggingSpan.start("checkout.submit")
  .add_metric("customer", {
    email: "person@example.com",
    phone: "+1-555-0100",
    apiKey: "secret-key",
  })
  .close();

console.log(closed.data.customer);
ts
{
  email: "pe***@example.com",
  phone: "+1***100",
  apiKey: "[redacted]"
}

Sanitization is a convenience layer, not a security boundary. Nested object values are cloned but not recursively sanitized.

Special Values ​

Lumberjack handles common runtime values before adding them to the Log Payload:

  • Error and TypeError values become { detail, stack, cause }.
  • Response values become { status, statusText, headers } with sanitized headers.
  • Objects are cloned and sanitized by top-level key.
  • Primitive values are stored as-is.

API At A Glance ​

APIDescription
LoggingSpan.start(name)Starts a Logging Span and records span.name and span.time_start.
new LoggingSpan(name)Direct constructor. Prefer LoggingSpan.start() in examples and application code.
span.add_metric(key, value)Adds or replaces one Metric.
span.add_bulk_metrics(metrics)Adds or replaces several Metrics.
span.set_level(level)Sets the level used by log_close() and exposed on the Closed Logging Span.
span.close()Closes the span and returns a Closed Logging Span without writing to console.
span.log_close()Closes the span, logs closed.data through console[closed.level], and returns the Closed Logging Span.
create_metric_namespace(name)Creates reusable helpers for namespaced Metric names.

Runtime Requirements ​

Lumberjack is an ESM TypeScript package for modern runtimes that provide these globals:

  • console
  • structuredClone
  • Response

The package also exports test stubs from @luckybeard/lumberjack/testing. Keep runtime usage focused on @luckybeard/lumberjack; use the testing entrypoint when you want to replace real logging spans in tests.