LogTape transport
This transport is introduced in Upyo 0.6.0.
The LogTape transport records email delivery lifecycle events as structured LogTape logs. It can be used by itself during local development, where no message is actually delivered, or as a decorator around another transport. In decorator mode, the wrapped transport still performs delivery and its receipts and errors pass through unchanged.
Upyo does not configure LogTape on behalf of the application. This follows LogTape's library-first design and leaves sinks, filters, and category levels under application control.
Installation
Install @upyo/logtape together with LogTape:
npm add @upyo/logtape @logtape/logtapepnpm add @upyo/logtape @logtape/logtapeyarn add @upyo/logtape @logtape/logtapedeno add jsr:@upyo/logtape jsr:@logtape/logtapebun add @upyo/logtape @logtape/logtapeLog-only usage
With no wrapped transport, LogTapeTransport records the send and returns a synthetic successful receipt. This is useful for exercising email workflows without sending real messages:
import { configure, getConsoleSink } from "@logtape/logtape";
import { createMessage } from "@upyo/core";
import { LogTapeTransport } from "@upyo/logtape";
await configure({
sinks: { console: getConsoleSink() },
loggers: [
{ category: ["upyo"], lowestLevel: "debug", sinks: ["console"] },
],
});
const transport = new LogTapeTransport();
const message = createMessage({
from: "sender@example.com",
to: "recipient@example.net",
subject: "Welcome",
content: { text: "Welcome to our service." },
});
const receipt = await transport.send(message);
if (receipt.successful) {
console.log(receipt.messageId); // "logtape-..."
}The default category is ["upyo"]. A log-only receipt uses the provider id "logtape" and a generated message id, but it does not imply that an email was delivered outside the application.
Decorating another transport
Pass another Upyo transport through the transport option to add logs around real delivery:
import { LogTapeTransport } from "@upyo/logtape";
import { SmtpTransport } from "@upyo/smtp";
const smtp = new SmtpTransport({
host: "smtp.example.com",
port: 587,
secure: false,
auth: {
user: "smtp-user@example.com",
pass: "smtp-password",
},
});
const transport = new LogTapeTransport({
transport: smtp,
category: ["application", "email"],
});The decorator preserves the wrapped provider id, forwards AbortSignal, uses the wrapped sendMany() implementation, and passes successful and failed receipts through unchanged. Exceptions are logged with the original error object and then rethrown. Explicit disposal is also forwarded to disposable wrapped transports. Completion logs are emitted as callers consume receipts. If a caller stops reading a batch early, the wrapped iterator is closed without being drained so the logging layer does not start additional delivery work.
LogTape transport can be nested with retry, pool, and OpenTelemetry transports. The outer decorator observes the complete operation performed by the inner transport, so choose the order according to which retries or failovers should appear as one logged operation.
Categories and levels
The lifecycle levels can be changed independently:
import { LogTapeTransport } from "@upyo/logtape";
const transport = new LogTapeTransport({
category: ["my-app", "outbound-email"],
levels: {
sending: "trace",
sent: "info",
failed: "fatal",
},
});sending- Level for
email.sendingevents. Defaults to"debug". sent- Level for
email.sentevents. Defaults to"info". failed- Level for failed receipts and thrown errors. Defaults to
"error".
Every event includes the operation, transport id, recipient counts, attachment count, and priority. Completion events additionally include duration and receipt or error details.
Recording message content
Messages are excluded from logs by default. The recordMessage option has two modes:
"properties"- Adds the complete
Messageobject to the structured properties of every lifecycle event. The log message itself remains on one line. "inline"- Adds the same
messageproperty and renders the subject and body beneath the lifecycle message. This format is convenient when reading local development logs.
Use "properties" when a sink will process the message as structured data:
import { LogTapeTransport } from "@upyo/logtape";
const transport = new LogTapeTransport({
recordMessage: "properties",
});Use "inline" to include the subject and body in the rendered log output:
import { LogTapeTransport } from "@upyo/logtape";
const transport = new LogTapeTransport({
recordMessage: "inline",
});Inline mode uses plain text whenever the text property is defined, even when it is an empty string. If no plain-text body is defined, it uses the HTML body. The subject and body remain LogTape placeholders instead of being combined into a string before logging, so sinks retain control over value rendering and redaction. Lifecycle errors without an associated message stay on one line.
CAUTION
Both modes expose complete messages. These can contain personal addresses, subjects, email bodies, custom headers, and large attachment data. Enable either mode only for sinks with suitable access controls and redaction.