Skip to content

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/logtape
pnpm add @upyo/logtape @logtape/logtape
yarn add @upyo/logtape @logtape/logtape
deno add jsr:@upyo/logtape jsr:@logtape/logtape
bun add @upyo/logtape @logtape/logtape

Log-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.sending events. Defaults to "debug".
sent
Level for email.sent events. 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 Message object to the structured properties of every lifecycle event. The log message itself remains on one line.
"inline"
Adds the same message property 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.