@upyo/logtape 0.6.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +58 -0
- package/dist/index.cjs +329 -0
- package/dist/index.d.cts +157 -0
- package/dist/index.d.ts +157 -0
- package/dist/index.js +306 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2025 Hong Minhee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<!-- deno-fmt-ignore-file -->
|
|
2
|
+
|
|
3
|
+
@upyo/logtape
|
|
4
|
+
=============
|
|
5
|
+
|
|
6
|
+
*@upyo/logtape* records Upyo email delivery lifecycle events through
|
|
7
|
+
[LogTape]. It can act as a log-only transport during local development or
|
|
8
|
+
decorate another transport while preserving its delivery behavior.
|
|
9
|
+
|
|
10
|
+
[LogTape]: https://logtape.org/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Installation
|
|
14
|
+
------------
|
|
15
|
+
|
|
16
|
+
~~~~ bash
|
|
17
|
+
deno add jsr:@upyo/logtape jsr:@logtape/logtape
|
|
18
|
+
pnpm add @upyo/logtape @logtape/logtape
|
|
19
|
+
~~~~
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Usage
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
Configure LogTape in your application, then create a log-only transport:
|
|
26
|
+
|
|
27
|
+
~~~~ typescript
|
|
28
|
+
import { configure, getConsoleSink } from "@logtape/logtape";
|
|
29
|
+
import { LogTapeTransport } from "@upyo/logtape";
|
|
30
|
+
|
|
31
|
+
await configure({
|
|
32
|
+
sinks: { console: getConsoleSink() },
|
|
33
|
+
loggers: [
|
|
34
|
+
{ category: ["upyo"], lowestLevel: "debug", sinks: ["console"] },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const transport = new LogTapeTransport();
|
|
39
|
+
const receipt = await transport.send(message);
|
|
40
|
+
~~~~
|
|
41
|
+
|
|
42
|
+
To deliver messages through another transport, pass it in the options object:
|
|
43
|
+
|
|
44
|
+
~~~~ typescript
|
|
45
|
+
const transport = new LogTapeTransport({
|
|
46
|
+
transport: smtpTransport,
|
|
47
|
+
category: ["application", "email"],
|
|
48
|
+
levels: {
|
|
49
|
+
sending: "debug",
|
|
50
|
+
sent: "info",
|
|
51
|
+
failed: "error",
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
~~~~
|
|
55
|
+
|
|
56
|
+
Complete messages are excluded from structured logs by default. Set
|
|
57
|
+
`recordMessage: true` only when the configured sinks and redaction rules can
|
|
58
|
+
safely handle addresses, bodies, headers, and attachment data.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
const __logtape_logtape = __toESM(require("@logtape/logtape"));
|
|
25
|
+
|
|
26
|
+
//#region src/config.ts
|
|
27
|
+
/**
|
|
28
|
+
* Resolves LogTape transport options with their defaults.
|
|
29
|
+
*
|
|
30
|
+
* @param options User-provided transport options.
|
|
31
|
+
* @returns Fully resolved transport options.
|
|
32
|
+
* @since 0.6.0
|
|
33
|
+
*/
|
|
34
|
+
function resolveLogTapeTransportOptions(options) {
|
|
35
|
+
const category = options.category ?? ["upyo"];
|
|
36
|
+
return {
|
|
37
|
+
transport: options.transport,
|
|
38
|
+
category: typeof category === "string" ? [category] : [...category],
|
|
39
|
+
recordMessage: options.recordMessage ?? false,
|
|
40
|
+
levels: {
|
|
41
|
+
sending: options.levels?.sending ?? "debug",
|
|
42
|
+
sent: options.levels?.sent ?? "info",
|
|
43
|
+
failed: options.levels?.failed ?? "error"
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/pending-queue.ts
|
|
50
|
+
/**
|
|
51
|
+
* Internal linked FIFO queue for messages awaiting delivery receipts.
|
|
52
|
+
*
|
|
53
|
+
* Both enqueue and dequeue operations take constant time, without retaining
|
|
54
|
+
* references to values that have already been dequeued.
|
|
55
|
+
*
|
|
56
|
+
* @typeParam T The queued value type.
|
|
57
|
+
* @since 0.6.0
|
|
58
|
+
*/
|
|
59
|
+
var PendingQueue = class {
|
|
60
|
+
head;
|
|
61
|
+
tail;
|
|
62
|
+
valueCount = 0;
|
|
63
|
+
/** Number of values waiting in the queue. */
|
|
64
|
+
get size() {
|
|
65
|
+
return this.valueCount;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Adds a value to the end of the queue.
|
|
69
|
+
*
|
|
70
|
+
* @param value The value to enqueue.
|
|
71
|
+
*/
|
|
72
|
+
enqueue(value) {
|
|
73
|
+
const node = { value };
|
|
74
|
+
if (this.tail == null) this.head = node;
|
|
75
|
+
else this.tail.next = node;
|
|
76
|
+
this.tail = node;
|
|
77
|
+
this.valueCount++;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Removes and returns the value at the front of the queue.
|
|
81
|
+
*
|
|
82
|
+
* @returns The first queued value, or `undefined` when the queue is empty.
|
|
83
|
+
*/
|
|
84
|
+
dequeue() {
|
|
85
|
+
const node = this.head;
|
|
86
|
+
if (node == null) return void 0;
|
|
87
|
+
this.head = node.next;
|
|
88
|
+
node.next = void 0;
|
|
89
|
+
this.valueCount--;
|
|
90
|
+
if (this.head == null) this.tail = void 0;
|
|
91
|
+
return node.value;
|
|
92
|
+
}
|
|
93
|
+
/** Iterates over queued values without removing them. */
|
|
94
|
+
*[Symbol.iterator]() {
|
|
95
|
+
let node = this.head;
|
|
96
|
+
while (node != null) {
|
|
97
|
+
yield node.value;
|
|
98
|
+
node = node.next;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/logtape-transport.ts
|
|
105
|
+
/**
|
|
106
|
+
* Transport that records email delivery lifecycle events through LogTape.
|
|
107
|
+
*
|
|
108
|
+
* With no wrapped transport, this class acts as a log-only transport and
|
|
109
|
+
* returns synthetic successful receipts. When a transport is supplied, it
|
|
110
|
+
* decorates that transport without changing its receipts or thrown errors.
|
|
111
|
+
*
|
|
112
|
+
* @typeParam TProviderId The provider id of the wrapped transport, or
|
|
113
|
+
* `"logtape"` in log-only mode.
|
|
114
|
+
* @since 0.6.0
|
|
115
|
+
*/
|
|
116
|
+
var LogTapeTransport = class {
|
|
117
|
+
/** Provider id used by receipts from this transport. */
|
|
118
|
+
id;
|
|
119
|
+
/** Fully resolved transport options. */
|
|
120
|
+
config;
|
|
121
|
+
logger;
|
|
122
|
+
wrappedTransport;
|
|
123
|
+
/**
|
|
124
|
+
* Creates a LogTape transport.
|
|
125
|
+
*
|
|
126
|
+
* @param options Logging and optional wrapped transport configuration.
|
|
127
|
+
*/
|
|
128
|
+
constructor(...[options]) {
|
|
129
|
+
this.config = resolveLogTapeTransportOptions(options ?? {});
|
|
130
|
+
this.wrappedTransport = this.config.transport;
|
|
131
|
+
this.id = this.wrappedTransport?.id ?? "logtape";
|
|
132
|
+
this.logger = (0, __logtape_logtape.getLogger)(this.config.category);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Sends one email while recording its delivery lifecycle.
|
|
136
|
+
*
|
|
137
|
+
* @param message The email message to send.
|
|
138
|
+
* @param options Optional transport options, including cancellation.
|
|
139
|
+
* @returns The wrapped transport receipt, or a synthetic successful receipt
|
|
140
|
+
* in log-only mode.
|
|
141
|
+
* @throws {DOMException} If the operation is aborted.
|
|
142
|
+
* @throws {Error} If the wrapped transport throws an error.
|
|
143
|
+
*/
|
|
144
|
+
send(message, options) {
|
|
145
|
+
return this.sendOne(message, options, "send");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Sends multiple emails while recording each delivery lifecycle.
|
|
149
|
+
*
|
|
150
|
+
* A wrapped transport's `sendMany()` implementation is used directly so
|
|
151
|
+
* provider-specific batching and streaming behavior are preserved.
|
|
152
|
+
*
|
|
153
|
+
* @param messages Email messages to send.
|
|
154
|
+
* @param options Optional transport options, including cancellation.
|
|
155
|
+
* @returns An async iterable of unmodified delivery receipts.
|
|
156
|
+
* @throws {DOMException} If the operation is aborted.
|
|
157
|
+
* @throws {Error} If the wrapped transport throws an error.
|
|
158
|
+
*/
|
|
159
|
+
async *sendMany(messages, options) {
|
|
160
|
+
options?.signal?.throwIfAborted();
|
|
161
|
+
if (this.wrappedTransport == null) {
|
|
162
|
+
for await (const message of messages) yield await this.sendOne(message, options, "sendMany");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const pending = new PendingQueue();
|
|
166
|
+
const batchStartedAt = performance.now();
|
|
167
|
+
let consumedCount = 0;
|
|
168
|
+
let completedCount = 0;
|
|
169
|
+
const observedMessages = this.observeMessages(messages, pending, options, () => consumedCount++);
|
|
170
|
+
try {
|
|
171
|
+
for await (const receipt of this.wrappedTransport.sendMany(observedMessages, options)) {
|
|
172
|
+
const current = pending.dequeue();
|
|
173
|
+
completedCount++;
|
|
174
|
+
this.logReceipt(receipt, current, "sendMany");
|
|
175
|
+
yield receipt;
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
const batchProperties = {
|
|
179
|
+
consumedCount,
|
|
180
|
+
completedCount,
|
|
181
|
+
pendingCount: pending.size
|
|
182
|
+
};
|
|
183
|
+
if (pending.size < 1) this.logThrownError(error, void 0, "sendMany", batchStartedAt, batchProperties);
|
|
184
|
+
else for (const pendingMessage of pending) this.logThrownError(error, pendingMessage.message, "sendMany", pendingMessage.startedAt, batchProperties);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Disposes the wrapped transport when it supports explicit resource
|
|
190
|
+
* management.
|
|
191
|
+
*/
|
|
192
|
+
async [Symbol.asyncDispose]() {
|
|
193
|
+
const transport = this.wrappedTransport;
|
|
194
|
+
if (transport == null) return;
|
|
195
|
+
if (isAsyncDisposable(transport)) {
|
|
196
|
+
await transport[Symbol.asyncDispose]();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (isDisposable(transport)) transport[Symbol.dispose]();
|
|
200
|
+
}
|
|
201
|
+
async sendOne(message, options, operation) {
|
|
202
|
+
options?.signal?.throwIfAborted();
|
|
203
|
+
const startedAt = performance.now();
|
|
204
|
+
this.logSending(message, operation);
|
|
205
|
+
try {
|
|
206
|
+
const receipt = this.wrappedTransport == null ? this.createSyntheticReceipt() : await this.wrappedTransport.send(message, options);
|
|
207
|
+
this.logReceipt(receipt, {
|
|
208
|
+
message,
|
|
209
|
+
startedAt
|
|
210
|
+
}, operation);
|
|
211
|
+
return receipt;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this.logThrownError(error, message, operation, startedAt);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async *observeMessages(messages, pending, options, onConsume) {
|
|
218
|
+
for await (const message of messages) {
|
|
219
|
+
options?.signal?.throwIfAborted();
|
|
220
|
+
const startedAt = performance.now();
|
|
221
|
+
this.logSending(message, "sendMany");
|
|
222
|
+
pending.enqueue({
|
|
223
|
+
message,
|
|
224
|
+
startedAt
|
|
225
|
+
});
|
|
226
|
+
onConsume();
|
|
227
|
+
yield message;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
createSyntheticReceipt() {
|
|
231
|
+
return {
|
|
232
|
+
successful: true,
|
|
233
|
+
messageId: `logtape-${crypto.randomUUID()}`,
|
|
234
|
+
provider: this.id,
|
|
235
|
+
attempts: 1,
|
|
236
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
logSending(message, operation) {
|
|
240
|
+
this.log(this.config.levels.sending, "Sending email.", {
|
|
241
|
+
...this.getMessageProperties(message),
|
|
242
|
+
event: "email.sending",
|
|
243
|
+
operation,
|
|
244
|
+
transportId: this.id
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
logReceipt(receipt, pending, operation) {
|
|
248
|
+
const durationMilliseconds = pending == null ? void 0 : performance.now() - pending.startedAt;
|
|
249
|
+
const messageProperties = pending == null ? {} : this.getMessageProperties(pending.message);
|
|
250
|
+
if (receipt.successful) {
|
|
251
|
+
this.log(this.config.levels.sent, "Email sent.", {
|
|
252
|
+
...messageProperties,
|
|
253
|
+
event: "email.sent",
|
|
254
|
+
operation,
|
|
255
|
+
transportId: this.id,
|
|
256
|
+
durationMilliseconds,
|
|
257
|
+
messageId: receipt.messageId,
|
|
258
|
+
provider: receipt.provider ?? this.id,
|
|
259
|
+
receipt
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
this.log(this.config.levels.failed, "Failed to send email.", {
|
|
264
|
+
...messageProperties,
|
|
265
|
+
event: "email.failed",
|
|
266
|
+
operation,
|
|
267
|
+
transportId: this.id,
|
|
268
|
+
durationMilliseconds,
|
|
269
|
+
errorMessages: receipt.errorMessages,
|
|
270
|
+
errors: receipt.errors,
|
|
271
|
+
retryable: receipt.retryable,
|
|
272
|
+
provider: receipt.provider ?? this.id,
|
|
273
|
+
attempts: receipt.attempts,
|
|
274
|
+
receipt
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
logThrownError(error, message, operation, startedAt, extraProperties = {}) {
|
|
278
|
+
this.log(this.config.levels.failed, "Failed to send email: {error}", {
|
|
279
|
+
...message == null ? {} : this.getMessageProperties(message),
|
|
280
|
+
...extraProperties,
|
|
281
|
+
event: "email.failed",
|
|
282
|
+
operation,
|
|
283
|
+
transportId: this.id,
|
|
284
|
+
durationMilliseconds: performance.now() - startedAt,
|
|
285
|
+
error
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
getMessageProperties(message) {
|
|
289
|
+
return {
|
|
290
|
+
recipientCount: message.recipients.length,
|
|
291
|
+
ccRecipientCount: message.ccRecipients.length,
|
|
292
|
+
bccRecipientCount: message.bccRecipients.length,
|
|
293
|
+
attachmentCount: message.attachments.length,
|
|
294
|
+
priority: message.priority,
|
|
295
|
+
...this.config.recordMessage ? { message } : {}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
log(level, message, properties) {
|
|
299
|
+
switch (level) {
|
|
300
|
+
case "trace":
|
|
301
|
+
this.logger.trace(message, properties);
|
|
302
|
+
break;
|
|
303
|
+
case "debug":
|
|
304
|
+
this.logger.debug(message, properties);
|
|
305
|
+
break;
|
|
306
|
+
case "info":
|
|
307
|
+
this.logger.info(message, properties);
|
|
308
|
+
break;
|
|
309
|
+
case "warning":
|
|
310
|
+
this.logger.warning(message, properties);
|
|
311
|
+
break;
|
|
312
|
+
case "error":
|
|
313
|
+
this.logger.error(message, properties);
|
|
314
|
+
break;
|
|
315
|
+
case "fatal":
|
|
316
|
+
this.logger.fatal(message, properties);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
function isAsyncDisposable(value) {
|
|
322
|
+
return Symbol.asyncDispose in value && typeof value[Symbol.asyncDispose] === "function";
|
|
323
|
+
}
|
|
324
|
+
function isDisposable(value) {
|
|
325
|
+
return Symbol.dispose in value && typeof value[Symbol.dispose] === "function";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
//#endregion
|
|
329
|
+
exports.LogTapeTransport = LogTapeTransport;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Message, Receipt, Transport, TransportOptions } from "@upyo/core";
|
|
2
|
+
import { LogLevel } from "@logtape/logtape";
|
|
3
|
+
|
|
4
|
+
//#region src/config.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Log levels used for email delivery lifecycle events.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.6.0
|
|
10
|
+
*/
|
|
11
|
+
interface LogTapeTransportLevels {
|
|
12
|
+
/**
|
|
13
|
+
* Level used before an email is handed to the transport.
|
|
14
|
+
*
|
|
15
|
+
* @default "debug"
|
|
16
|
+
*/
|
|
17
|
+
readonly sending?: LogLevel;
|
|
18
|
+
/**
|
|
19
|
+
* Level used after an email is sent successfully.
|
|
20
|
+
*
|
|
21
|
+
* @default "info"
|
|
22
|
+
*/
|
|
23
|
+
readonly sent?: LogLevel;
|
|
24
|
+
/**
|
|
25
|
+
* Level used when a transport returns a failed receipt or throws.
|
|
26
|
+
*
|
|
27
|
+
* @default "error"
|
|
28
|
+
*/
|
|
29
|
+
readonly failed?: LogLevel;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Options for {@link LogTapeTransport}.
|
|
33
|
+
*
|
|
34
|
+
* @typeParam TProviderId The provider id of the wrapped transport.
|
|
35
|
+
* @since 0.6.0
|
|
36
|
+
*/
|
|
37
|
+
interface LogTapeTransportOptions<TProviderId extends string = "logtape"> {
|
|
38
|
+
/**
|
|
39
|
+
* Transport that performs the actual delivery.
|
|
40
|
+
*
|
|
41
|
+
* When omitted, the LogTape transport only logs the delivery and returns a
|
|
42
|
+
* synthetic successful receipt.
|
|
43
|
+
*/
|
|
44
|
+
readonly transport?: Transport<TProviderId>;
|
|
45
|
+
/**
|
|
46
|
+
* LogTape category used for delivery logs.
|
|
47
|
+
*
|
|
48
|
+
* @default ["upyo"]
|
|
49
|
+
*/
|
|
50
|
+
readonly category?: string | readonly string[];
|
|
51
|
+
/**
|
|
52
|
+
* Whether to include the complete email message in structured log
|
|
53
|
+
* properties.
|
|
54
|
+
*
|
|
55
|
+
* Messages may contain sensitive or large values, including email bodies,
|
|
56
|
+
* headers, and attachment data.
|
|
57
|
+
*
|
|
58
|
+
* @default false
|
|
59
|
+
*/
|
|
60
|
+
readonly recordMessage?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Levels used for delivery lifecycle events.
|
|
63
|
+
*/
|
|
64
|
+
readonly levels?: LogTapeTransportLevels;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Fully resolved options used by {@link LogTapeTransport}.
|
|
68
|
+
*
|
|
69
|
+
* @typeParam TProviderId The provider id of the wrapped transport.
|
|
70
|
+
* @since 0.6.0
|
|
71
|
+
*/
|
|
72
|
+
interface ResolvedLogTapeTransportOptions<TProviderId extends string = "logtape"> {
|
|
73
|
+
/** Transport that performs the actual delivery, if configured. */
|
|
74
|
+
readonly transport?: Transport<TProviderId>;
|
|
75
|
+
/** Resolved LogTape category. */
|
|
76
|
+
readonly category: readonly string[];
|
|
77
|
+
/** Whether complete messages are included in structured logs. */
|
|
78
|
+
readonly recordMessage: boolean;
|
|
79
|
+
/** Resolved delivery lifecycle log levels. */
|
|
80
|
+
readonly levels: Required<LogTapeTransportLevels>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolves LogTape transport options with their defaults.
|
|
84
|
+
*
|
|
85
|
+
* @param options User-provided transport options.
|
|
86
|
+
* @returns Fully resolved transport options.
|
|
87
|
+
* @since 0.6.0
|
|
88
|
+
*/
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/logtape-transport.d.ts
|
|
91
|
+
type LogTapeTransportConstructorArguments<TProviderId extends string> = [TProviderId] extends ["logtape"] ? [options?: LogTapeTransportOptions<TProviderId>] : [options: LogTapeTransportOptions<TProviderId> & {
|
|
92
|
+
readonly transport: Transport<TProviderId>;
|
|
93
|
+
}];
|
|
94
|
+
/**
|
|
95
|
+
* Transport that records email delivery lifecycle events through LogTape.
|
|
96
|
+
*
|
|
97
|
+
* With no wrapped transport, this class acts as a log-only transport and
|
|
98
|
+
* returns synthetic successful receipts. When a transport is supplied, it
|
|
99
|
+
* decorates that transport without changing its receipts or thrown errors.
|
|
100
|
+
*
|
|
101
|
+
* @typeParam TProviderId The provider id of the wrapped transport, or
|
|
102
|
+
* `"logtape"` in log-only mode.
|
|
103
|
+
* @since 0.6.0
|
|
104
|
+
*/
|
|
105
|
+
declare class LogTapeTransport<TProviderId extends string = "logtape"> implements Transport<TProviderId>, AsyncDisposable {
|
|
106
|
+
/** Provider id used by receipts from this transport. */
|
|
107
|
+
readonly id: TProviderId;
|
|
108
|
+
/** Fully resolved transport options. */
|
|
109
|
+
readonly config: ResolvedLogTapeTransportOptions<TProviderId>;
|
|
110
|
+
private readonly logger;
|
|
111
|
+
private readonly wrappedTransport?;
|
|
112
|
+
/**
|
|
113
|
+
* Creates a LogTape transport.
|
|
114
|
+
*
|
|
115
|
+
* @param options Logging and optional wrapped transport configuration.
|
|
116
|
+
*/
|
|
117
|
+
constructor(...[options]: LogTapeTransportConstructorArguments<TProviderId>);
|
|
118
|
+
/**
|
|
119
|
+
* Sends one email while recording its delivery lifecycle.
|
|
120
|
+
*
|
|
121
|
+
* @param message The email message to send.
|
|
122
|
+
* @param options Optional transport options, including cancellation.
|
|
123
|
+
* @returns The wrapped transport receipt, or a synthetic successful receipt
|
|
124
|
+
* in log-only mode.
|
|
125
|
+
* @throws {DOMException} If the operation is aborted.
|
|
126
|
+
* @throws {Error} If the wrapped transport throws an error.
|
|
127
|
+
*/
|
|
128
|
+
send(message: Message, options?: TransportOptions): Promise<Receipt<TProviderId>>;
|
|
129
|
+
/**
|
|
130
|
+
* Sends multiple emails while recording each delivery lifecycle.
|
|
131
|
+
*
|
|
132
|
+
* A wrapped transport's `sendMany()` implementation is used directly so
|
|
133
|
+
* provider-specific batching and streaming behavior are preserved.
|
|
134
|
+
*
|
|
135
|
+
* @param messages Email messages to send.
|
|
136
|
+
* @param options Optional transport options, including cancellation.
|
|
137
|
+
* @returns An async iterable of unmodified delivery receipts.
|
|
138
|
+
* @throws {DOMException} If the operation is aborted.
|
|
139
|
+
* @throws {Error} If the wrapped transport throws an error.
|
|
140
|
+
*/
|
|
141
|
+
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<TProviderId>>;
|
|
142
|
+
/**
|
|
143
|
+
* Disposes the wrapped transport when it supports explicit resource
|
|
144
|
+
* management.
|
|
145
|
+
*/
|
|
146
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
147
|
+
private sendOne;
|
|
148
|
+
private observeMessages;
|
|
149
|
+
private createSyntheticReceipt;
|
|
150
|
+
private logSending;
|
|
151
|
+
private logReceipt;
|
|
152
|
+
private logThrownError;
|
|
153
|
+
private getMessageProperties;
|
|
154
|
+
private log;
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
export { LogTapeTransport, LogTapeTransportLevels, LogTapeTransportOptions, ResolvedLogTapeTransportOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { LogLevel } from "@logtape/logtape";
|
|
2
|
+
import { Message, Receipt, Transport, TransportOptions } from "@upyo/core";
|
|
3
|
+
|
|
4
|
+
//#region src/config.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Log levels used for email delivery lifecycle events.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.6.0
|
|
10
|
+
*/
|
|
11
|
+
interface LogTapeTransportLevels {
|
|
12
|
+
/**
|
|
13
|
+
* Level used before an email is handed to the transport.
|
|
14
|
+
*
|
|
15
|
+
* @default "debug"
|
|
16
|
+
*/
|
|
17
|
+
readonly sending?: LogLevel;
|
|
18
|
+
/**
|
|
19
|
+
* Level used after an email is sent successfully.
|
|
20
|
+
*
|
|
21
|
+
* @default "info"
|
|
22
|
+
*/
|
|
23
|
+
readonly sent?: LogLevel;
|
|
24
|
+
/**
|
|
25
|
+
* Level used when a transport returns a failed receipt or throws.
|
|
26
|
+
*
|
|
27
|
+
* @default "error"
|
|
28
|
+
*/
|
|
29
|
+
readonly failed?: LogLevel;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Options for {@link LogTapeTransport}.
|
|
33
|
+
*
|
|
34
|
+
* @typeParam TProviderId The provider id of the wrapped transport.
|
|
35
|
+
* @since 0.6.0
|
|
36
|
+
*/
|
|
37
|
+
interface LogTapeTransportOptions<TProviderId extends string = "logtape"> {
|
|
38
|
+
/**
|
|
39
|
+
* Transport that performs the actual delivery.
|
|
40
|
+
*
|
|
41
|
+
* When omitted, the LogTape transport only logs the delivery and returns a
|
|
42
|
+
* synthetic successful receipt.
|
|
43
|
+
*/
|
|
44
|
+
readonly transport?: Transport<TProviderId>;
|
|
45
|
+
/**
|
|
46
|
+
* LogTape category used for delivery logs.
|
|
47
|
+
*
|
|
48
|
+
* @default ["upyo"]
|
|
49
|
+
*/
|
|
50
|
+
readonly category?: string | readonly string[];
|
|
51
|
+
/**
|
|
52
|
+
* Whether to include the complete email message in structured log
|
|
53
|
+
* properties.
|
|
54
|
+
*
|
|
55
|
+
* Messages may contain sensitive or large values, including email bodies,
|
|
56
|
+
* headers, and attachment data.
|
|
57
|
+
*
|
|
58
|
+
* @default false
|
|
59
|
+
*/
|
|
60
|
+
readonly recordMessage?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Levels used for delivery lifecycle events.
|
|
63
|
+
*/
|
|
64
|
+
readonly levels?: LogTapeTransportLevels;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Fully resolved options used by {@link LogTapeTransport}.
|
|
68
|
+
*
|
|
69
|
+
* @typeParam TProviderId The provider id of the wrapped transport.
|
|
70
|
+
* @since 0.6.0
|
|
71
|
+
*/
|
|
72
|
+
interface ResolvedLogTapeTransportOptions<TProviderId extends string = "logtape"> {
|
|
73
|
+
/** Transport that performs the actual delivery, if configured. */
|
|
74
|
+
readonly transport?: Transport<TProviderId>;
|
|
75
|
+
/** Resolved LogTape category. */
|
|
76
|
+
readonly category: readonly string[];
|
|
77
|
+
/** Whether complete messages are included in structured logs. */
|
|
78
|
+
readonly recordMessage: boolean;
|
|
79
|
+
/** Resolved delivery lifecycle log levels. */
|
|
80
|
+
readonly levels: Required<LogTapeTransportLevels>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolves LogTape transport options with their defaults.
|
|
84
|
+
*
|
|
85
|
+
* @param options User-provided transport options.
|
|
86
|
+
* @returns Fully resolved transport options.
|
|
87
|
+
* @since 0.6.0
|
|
88
|
+
*/
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/logtape-transport.d.ts
|
|
91
|
+
type LogTapeTransportConstructorArguments<TProviderId extends string> = [TProviderId] extends ["logtape"] ? [options?: LogTapeTransportOptions<TProviderId>] : [options: LogTapeTransportOptions<TProviderId> & {
|
|
92
|
+
readonly transport: Transport<TProviderId>;
|
|
93
|
+
}];
|
|
94
|
+
/**
|
|
95
|
+
* Transport that records email delivery lifecycle events through LogTape.
|
|
96
|
+
*
|
|
97
|
+
* With no wrapped transport, this class acts as a log-only transport and
|
|
98
|
+
* returns synthetic successful receipts. When a transport is supplied, it
|
|
99
|
+
* decorates that transport without changing its receipts or thrown errors.
|
|
100
|
+
*
|
|
101
|
+
* @typeParam TProviderId The provider id of the wrapped transport, or
|
|
102
|
+
* `"logtape"` in log-only mode.
|
|
103
|
+
* @since 0.6.0
|
|
104
|
+
*/
|
|
105
|
+
declare class LogTapeTransport<TProviderId extends string = "logtape"> implements Transport<TProviderId>, AsyncDisposable {
|
|
106
|
+
/** Provider id used by receipts from this transport. */
|
|
107
|
+
readonly id: TProviderId;
|
|
108
|
+
/** Fully resolved transport options. */
|
|
109
|
+
readonly config: ResolvedLogTapeTransportOptions<TProviderId>;
|
|
110
|
+
private readonly logger;
|
|
111
|
+
private readonly wrappedTransport?;
|
|
112
|
+
/**
|
|
113
|
+
* Creates a LogTape transport.
|
|
114
|
+
*
|
|
115
|
+
* @param options Logging and optional wrapped transport configuration.
|
|
116
|
+
*/
|
|
117
|
+
constructor(...[options]: LogTapeTransportConstructorArguments<TProviderId>);
|
|
118
|
+
/**
|
|
119
|
+
* Sends one email while recording its delivery lifecycle.
|
|
120
|
+
*
|
|
121
|
+
* @param message The email message to send.
|
|
122
|
+
* @param options Optional transport options, including cancellation.
|
|
123
|
+
* @returns The wrapped transport receipt, or a synthetic successful receipt
|
|
124
|
+
* in log-only mode.
|
|
125
|
+
* @throws {DOMException} If the operation is aborted.
|
|
126
|
+
* @throws {Error} If the wrapped transport throws an error.
|
|
127
|
+
*/
|
|
128
|
+
send(message: Message, options?: TransportOptions): Promise<Receipt<TProviderId>>;
|
|
129
|
+
/**
|
|
130
|
+
* Sends multiple emails while recording each delivery lifecycle.
|
|
131
|
+
*
|
|
132
|
+
* A wrapped transport's `sendMany()` implementation is used directly so
|
|
133
|
+
* provider-specific batching and streaming behavior are preserved.
|
|
134
|
+
*
|
|
135
|
+
* @param messages Email messages to send.
|
|
136
|
+
* @param options Optional transport options, including cancellation.
|
|
137
|
+
* @returns An async iterable of unmodified delivery receipts.
|
|
138
|
+
* @throws {DOMException} If the operation is aborted.
|
|
139
|
+
* @throws {Error} If the wrapped transport throws an error.
|
|
140
|
+
*/
|
|
141
|
+
sendMany(messages: Iterable<Message> | AsyncIterable<Message>, options?: TransportOptions): AsyncIterable<Receipt<TProviderId>>;
|
|
142
|
+
/**
|
|
143
|
+
* Disposes the wrapped transport when it supports explicit resource
|
|
144
|
+
* management.
|
|
145
|
+
*/
|
|
146
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
147
|
+
private sendOne;
|
|
148
|
+
private observeMessages;
|
|
149
|
+
private createSyntheticReceipt;
|
|
150
|
+
private logSending;
|
|
151
|
+
private logReceipt;
|
|
152
|
+
private logThrownError;
|
|
153
|
+
private getMessageProperties;
|
|
154
|
+
private log;
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
export { LogTapeTransport, LogTapeTransportLevels, LogTapeTransportOptions, ResolvedLogTapeTransportOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { getLogger } from "@logtape/logtape";
|
|
2
|
+
|
|
3
|
+
//#region src/config.ts
|
|
4
|
+
/**
|
|
5
|
+
* Resolves LogTape transport options with their defaults.
|
|
6
|
+
*
|
|
7
|
+
* @param options User-provided transport options.
|
|
8
|
+
* @returns Fully resolved transport options.
|
|
9
|
+
* @since 0.6.0
|
|
10
|
+
*/
|
|
11
|
+
function resolveLogTapeTransportOptions(options) {
|
|
12
|
+
const category = options.category ?? ["upyo"];
|
|
13
|
+
return {
|
|
14
|
+
transport: options.transport,
|
|
15
|
+
category: typeof category === "string" ? [category] : [...category],
|
|
16
|
+
recordMessage: options.recordMessage ?? false,
|
|
17
|
+
levels: {
|
|
18
|
+
sending: options.levels?.sending ?? "debug",
|
|
19
|
+
sent: options.levels?.sent ?? "info",
|
|
20
|
+
failed: options.levels?.failed ?? "error"
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/pending-queue.ts
|
|
27
|
+
/**
|
|
28
|
+
* Internal linked FIFO queue for messages awaiting delivery receipts.
|
|
29
|
+
*
|
|
30
|
+
* Both enqueue and dequeue operations take constant time, without retaining
|
|
31
|
+
* references to values that have already been dequeued.
|
|
32
|
+
*
|
|
33
|
+
* @typeParam T The queued value type.
|
|
34
|
+
* @since 0.6.0
|
|
35
|
+
*/
|
|
36
|
+
var PendingQueue = class {
|
|
37
|
+
head;
|
|
38
|
+
tail;
|
|
39
|
+
valueCount = 0;
|
|
40
|
+
/** Number of values waiting in the queue. */
|
|
41
|
+
get size() {
|
|
42
|
+
return this.valueCount;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Adds a value to the end of the queue.
|
|
46
|
+
*
|
|
47
|
+
* @param value The value to enqueue.
|
|
48
|
+
*/
|
|
49
|
+
enqueue(value) {
|
|
50
|
+
const node = { value };
|
|
51
|
+
if (this.tail == null) this.head = node;
|
|
52
|
+
else this.tail.next = node;
|
|
53
|
+
this.tail = node;
|
|
54
|
+
this.valueCount++;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Removes and returns the value at the front of the queue.
|
|
58
|
+
*
|
|
59
|
+
* @returns The first queued value, or `undefined` when the queue is empty.
|
|
60
|
+
*/
|
|
61
|
+
dequeue() {
|
|
62
|
+
const node = this.head;
|
|
63
|
+
if (node == null) return void 0;
|
|
64
|
+
this.head = node.next;
|
|
65
|
+
node.next = void 0;
|
|
66
|
+
this.valueCount--;
|
|
67
|
+
if (this.head == null) this.tail = void 0;
|
|
68
|
+
return node.value;
|
|
69
|
+
}
|
|
70
|
+
/** Iterates over queued values without removing them. */
|
|
71
|
+
*[Symbol.iterator]() {
|
|
72
|
+
let node = this.head;
|
|
73
|
+
while (node != null) {
|
|
74
|
+
yield node.value;
|
|
75
|
+
node = node.next;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/logtape-transport.ts
|
|
82
|
+
/**
|
|
83
|
+
* Transport that records email delivery lifecycle events through LogTape.
|
|
84
|
+
*
|
|
85
|
+
* With no wrapped transport, this class acts as a log-only transport and
|
|
86
|
+
* returns synthetic successful receipts. When a transport is supplied, it
|
|
87
|
+
* decorates that transport without changing its receipts or thrown errors.
|
|
88
|
+
*
|
|
89
|
+
* @typeParam TProviderId The provider id of the wrapped transport, or
|
|
90
|
+
* `"logtape"` in log-only mode.
|
|
91
|
+
* @since 0.6.0
|
|
92
|
+
*/
|
|
93
|
+
var LogTapeTransport = class {
|
|
94
|
+
/** Provider id used by receipts from this transport. */
|
|
95
|
+
id;
|
|
96
|
+
/** Fully resolved transport options. */
|
|
97
|
+
config;
|
|
98
|
+
logger;
|
|
99
|
+
wrappedTransport;
|
|
100
|
+
/**
|
|
101
|
+
* Creates a LogTape transport.
|
|
102
|
+
*
|
|
103
|
+
* @param options Logging and optional wrapped transport configuration.
|
|
104
|
+
*/
|
|
105
|
+
constructor(...[options]) {
|
|
106
|
+
this.config = resolveLogTapeTransportOptions(options ?? {});
|
|
107
|
+
this.wrappedTransport = this.config.transport;
|
|
108
|
+
this.id = this.wrappedTransport?.id ?? "logtape";
|
|
109
|
+
this.logger = getLogger(this.config.category);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Sends one email while recording its delivery lifecycle.
|
|
113
|
+
*
|
|
114
|
+
* @param message The email message to send.
|
|
115
|
+
* @param options Optional transport options, including cancellation.
|
|
116
|
+
* @returns The wrapped transport receipt, or a synthetic successful receipt
|
|
117
|
+
* in log-only mode.
|
|
118
|
+
* @throws {DOMException} If the operation is aborted.
|
|
119
|
+
* @throws {Error} If the wrapped transport throws an error.
|
|
120
|
+
*/
|
|
121
|
+
send(message, options) {
|
|
122
|
+
return this.sendOne(message, options, "send");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Sends multiple emails while recording each delivery lifecycle.
|
|
126
|
+
*
|
|
127
|
+
* A wrapped transport's `sendMany()` implementation is used directly so
|
|
128
|
+
* provider-specific batching and streaming behavior are preserved.
|
|
129
|
+
*
|
|
130
|
+
* @param messages Email messages to send.
|
|
131
|
+
* @param options Optional transport options, including cancellation.
|
|
132
|
+
* @returns An async iterable of unmodified delivery receipts.
|
|
133
|
+
* @throws {DOMException} If the operation is aborted.
|
|
134
|
+
* @throws {Error} If the wrapped transport throws an error.
|
|
135
|
+
*/
|
|
136
|
+
async *sendMany(messages, options) {
|
|
137
|
+
options?.signal?.throwIfAborted();
|
|
138
|
+
if (this.wrappedTransport == null) {
|
|
139
|
+
for await (const message of messages) yield await this.sendOne(message, options, "sendMany");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const pending = new PendingQueue();
|
|
143
|
+
const batchStartedAt = performance.now();
|
|
144
|
+
let consumedCount = 0;
|
|
145
|
+
let completedCount = 0;
|
|
146
|
+
const observedMessages = this.observeMessages(messages, pending, options, () => consumedCount++);
|
|
147
|
+
try {
|
|
148
|
+
for await (const receipt of this.wrappedTransport.sendMany(observedMessages, options)) {
|
|
149
|
+
const current = pending.dequeue();
|
|
150
|
+
completedCount++;
|
|
151
|
+
this.logReceipt(receipt, current, "sendMany");
|
|
152
|
+
yield receipt;
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const batchProperties = {
|
|
156
|
+
consumedCount,
|
|
157
|
+
completedCount,
|
|
158
|
+
pendingCount: pending.size
|
|
159
|
+
};
|
|
160
|
+
if (pending.size < 1) this.logThrownError(error, void 0, "sendMany", batchStartedAt, batchProperties);
|
|
161
|
+
else for (const pendingMessage of pending) this.logThrownError(error, pendingMessage.message, "sendMany", pendingMessage.startedAt, batchProperties);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Disposes the wrapped transport when it supports explicit resource
|
|
167
|
+
* management.
|
|
168
|
+
*/
|
|
169
|
+
async [Symbol.asyncDispose]() {
|
|
170
|
+
const transport = this.wrappedTransport;
|
|
171
|
+
if (transport == null) return;
|
|
172
|
+
if (isAsyncDisposable(transport)) {
|
|
173
|
+
await transport[Symbol.asyncDispose]();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (isDisposable(transport)) transport[Symbol.dispose]();
|
|
177
|
+
}
|
|
178
|
+
async sendOne(message, options, operation) {
|
|
179
|
+
options?.signal?.throwIfAborted();
|
|
180
|
+
const startedAt = performance.now();
|
|
181
|
+
this.logSending(message, operation);
|
|
182
|
+
try {
|
|
183
|
+
const receipt = this.wrappedTransport == null ? this.createSyntheticReceipt() : await this.wrappedTransport.send(message, options);
|
|
184
|
+
this.logReceipt(receipt, {
|
|
185
|
+
message,
|
|
186
|
+
startedAt
|
|
187
|
+
}, operation);
|
|
188
|
+
return receipt;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.logThrownError(error, message, operation, startedAt);
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async *observeMessages(messages, pending, options, onConsume) {
|
|
195
|
+
for await (const message of messages) {
|
|
196
|
+
options?.signal?.throwIfAborted();
|
|
197
|
+
const startedAt = performance.now();
|
|
198
|
+
this.logSending(message, "sendMany");
|
|
199
|
+
pending.enqueue({
|
|
200
|
+
message,
|
|
201
|
+
startedAt
|
|
202
|
+
});
|
|
203
|
+
onConsume();
|
|
204
|
+
yield message;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
createSyntheticReceipt() {
|
|
208
|
+
return {
|
|
209
|
+
successful: true,
|
|
210
|
+
messageId: `logtape-${crypto.randomUUID()}`,
|
|
211
|
+
provider: this.id,
|
|
212
|
+
attempts: 1,
|
|
213
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
logSending(message, operation) {
|
|
217
|
+
this.log(this.config.levels.sending, "Sending email.", {
|
|
218
|
+
...this.getMessageProperties(message),
|
|
219
|
+
event: "email.sending",
|
|
220
|
+
operation,
|
|
221
|
+
transportId: this.id
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
logReceipt(receipt, pending, operation) {
|
|
225
|
+
const durationMilliseconds = pending == null ? void 0 : performance.now() - pending.startedAt;
|
|
226
|
+
const messageProperties = pending == null ? {} : this.getMessageProperties(pending.message);
|
|
227
|
+
if (receipt.successful) {
|
|
228
|
+
this.log(this.config.levels.sent, "Email sent.", {
|
|
229
|
+
...messageProperties,
|
|
230
|
+
event: "email.sent",
|
|
231
|
+
operation,
|
|
232
|
+
transportId: this.id,
|
|
233
|
+
durationMilliseconds,
|
|
234
|
+
messageId: receipt.messageId,
|
|
235
|
+
provider: receipt.provider ?? this.id,
|
|
236
|
+
receipt
|
|
237
|
+
});
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
this.log(this.config.levels.failed, "Failed to send email.", {
|
|
241
|
+
...messageProperties,
|
|
242
|
+
event: "email.failed",
|
|
243
|
+
operation,
|
|
244
|
+
transportId: this.id,
|
|
245
|
+
durationMilliseconds,
|
|
246
|
+
errorMessages: receipt.errorMessages,
|
|
247
|
+
errors: receipt.errors,
|
|
248
|
+
retryable: receipt.retryable,
|
|
249
|
+
provider: receipt.provider ?? this.id,
|
|
250
|
+
attempts: receipt.attempts,
|
|
251
|
+
receipt
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
logThrownError(error, message, operation, startedAt, extraProperties = {}) {
|
|
255
|
+
this.log(this.config.levels.failed, "Failed to send email: {error}", {
|
|
256
|
+
...message == null ? {} : this.getMessageProperties(message),
|
|
257
|
+
...extraProperties,
|
|
258
|
+
event: "email.failed",
|
|
259
|
+
operation,
|
|
260
|
+
transportId: this.id,
|
|
261
|
+
durationMilliseconds: performance.now() - startedAt,
|
|
262
|
+
error
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
getMessageProperties(message) {
|
|
266
|
+
return {
|
|
267
|
+
recipientCount: message.recipients.length,
|
|
268
|
+
ccRecipientCount: message.ccRecipients.length,
|
|
269
|
+
bccRecipientCount: message.bccRecipients.length,
|
|
270
|
+
attachmentCount: message.attachments.length,
|
|
271
|
+
priority: message.priority,
|
|
272
|
+
...this.config.recordMessage ? { message } : {}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
log(level, message, properties) {
|
|
276
|
+
switch (level) {
|
|
277
|
+
case "trace":
|
|
278
|
+
this.logger.trace(message, properties);
|
|
279
|
+
break;
|
|
280
|
+
case "debug":
|
|
281
|
+
this.logger.debug(message, properties);
|
|
282
|
+
break;
|
|
283
|
+
case "info":
|
|
284
|
+
this.logger.info(message, properties);
|
|
285
|
+
break;
|
|
286
|
+
case "warning":
|
|
287
|
+
this.logger.warning(message, properties);
|
|
288
|
+
break;
|
|
289
|
+
case "error":
|
|
290
|
+
this.logger.error(message, properties);
|
|
291
|
+
break;
|
|
292
|
+
case "fatal":
|
|
293
|
+
this.logger.fatal(message, properties);
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
function isAsyncDisposable(value) {
|
|
299
|
+
return Symbol.asyncDispose in value && typeof value[Symbol.asyncDispose] === "function";
|
|
300
|
+
}
|
|
301
|
+
function isDisposable(value) {
|
|
302
|
+
return Symbol.dispose in value && typeof value[Symbol.dispose] === "function";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
//#endregion
|
|
306
|
+
export { LogTapeTransport };
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@upyo/logtape",
|
|
3
|
+
"version": "0.6.0-dev.0",
|
|
4
|
+
"description": "LogTape observability transport for Upyo email library",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"email",
|
|
7
|
+
"mail",
|
|
8
|
+
"logtape",
|
|
9
|
+
"logging",
|
|
10
|
+
"observability"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Hong Minhee",
|
|
15
|
+
"email": "hong@minhee.org",
|
|
16
|
+
"url": "https://hongminhee.org/"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://upyo.org/transports/logtape",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/dahlia/upyo.git",
|
|
22
|
+
"directory": "packages/logtape/"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/dahlia/upyo/issues"
|
|
26
|
+
},
|
|
27
|
+
"funding": [
|
|
28
|
+
"https://github.com/sponsors/dahlia"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20.0.0",
|
|
32
|
+
"bun": ">=1.2.0",
|
|
33
|
+
"deno": ">=2.3.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/",
|
|
37
|
+
"package.json",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"type": "module",
|
|
41
|
+
"module": "./dist/index.js",
|
|
42
|
+
"main": "./dist/index.cjs",
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": {
|
|
47
|
+
"import": "./dist/index.d.ts",
|
|
48
|
+
"require": "./dist/index.d.cts"
|
|
49
|
+
},
|
|
50
|
+
"import": "./dist/index.js",
|
|
51
|
+
"require": "./dist/index.cjs"
|
|
52
|
+
},
|
|
53
|
+
"./package.json": "./package.json"
|
|
54
|
+
},
|
|
55
|
+
"sideEffects": false,
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@logtape/logtape": "^2.2.4",
|
|
58
|
+
"@upyo/core": "0.6.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@logtape/logtape": "^2.2.4",
|
|
62
|
+
"@logtape/testing": "^2.2.4",
|
|
63
|
+
"tsdown": "^0.12.7",
|
|
64
|
+
"typescript": "5.8.3"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"prepublish": "mise run --no-deps :build"
|
|
68
|
+
}
|
|
69
|
+
}
|