@upyo/opentelemetry 0.2.0-dev.24

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/README.md ADDED
@@ -0,0 +1,449 @@
1
+ <!-- deno-fmt-ignore-file -->
2
+
3
+ @upyo/opentelemetry
4
+ ===================
5
+
6
+ [![JSR][JSR badge]][JSR]
7
+ [![npm][npm badge]][npm]
8
+
9
+ OpenTelemetry observability transport for [Upyo] email library.
10
+
11
+ This package provides a decorator transport that wraps existing Upyo transports
12
+ to add automatic OpenTelemetry metrics and tracing without requiring changes to
13
+ existing code. It follows the decorator pattern, accepting any transport and
14
+ adding standardized observability features including email delivery metrics,
15
+ latency histograms, error classification, and distributed tracing support.
16
+
17
+ [JSR]: https://jsr.io/@upyo/opentelemetry
18
+ [JSR badge]: https://jsr.io/badges/@upyo/opentelemetry
19
+ [npm]: https://www.npmjs.com/package/@upyo/opentelemetry
20
+ [npm badge]: https://img.shields.io/npm/v/@upyo/opentelemetry?logo=npm
21
+ [Upyo]: https://upyo.org/
22
+
23
+
24
+ Features
25
+ --------
26
+
27
+ - Zero-code observability: Wrap any existing Upyo transport with OpenTelemetry
28
+ instrumentation
29
+ - Comprehensive metrics: Email delivery counters, duration histograms,
30
+ message size tracking, and active operation gauges
31
+ - Distributed tracing: Automatic span creation with semantic attributes
32
+ following OpenTelemetry conventions
33
+ - Error classification: Intelligent categorization of email delivery failures
34
+ (auth, rate_limit, network, etc.)
35
+ - Performance optimized: Configurable sampling rates and feature toggles for
36
+ minimal overhead
37
+ - Cross-runtime compatible: Works on Node.js, Deno, Bun, and edge functions
38
+ - Flexible configuration: Support both manual provider injection and
39
+ auto-configuration helpers
40
+
41
+
42
+ Installation
43
+ ------------
44
+
45
+ ~~~~ sh
46
+ npm add @upyo/core @upyo/opentelemetry @opentelemetry/api
47
+ pnpm add @upyo/core @upyo/opentelemetry @opentelemetry/api
48
+ yarn add @upyo/core @upyo/opentelemetry @opentelemetry/api
49
+ deno add --jsr @upyo/core @upyo/opentelemetry
50
+ bun add @upyo/core @upyo/opentelemetry @opentelemetry/api
51
+ ~~~~
52
+
53
+
54
+ Basic Usage
55
+ -----------
56
+
57
+ ~~~~ typescript
58
+ import { MailgunTransport } from "@upyo/mailgun";
59
+ import { OpenTelemetryTransport } from "@upyo/opentelemetry";
60
+ import { trace, metrics } from "@opentelemetry/api";
61
+
62
+ // Create your base transport
63
+ const baseTransport = new MailgunTransport({
64
+ apiKey: "your-api-key",
65
+ domain: "your-domain.com",
66
+ region: "us"
67
+ });
68
+
69
+ // Wrap with OpenTelemetry observability
70
+ const transport = new OpenTelemetryTransport(baseTransport, {
71
+ tracerProvider: trace.getTracerProvider(),
72
+ meterProvider: metrics.getMeterProvider(),
73
+ metrics: { enabled: true },
74
+ tracing: { enabled: true }
75
+ });
76
+
77
+ // Use exactly like any other transport
78
+ const receipt = await transport.send(message);
79
+ if (receipt.successful) {
80
+ console.log('Message sent with ID:', receipt.messageId);
81
+ } else {
82
+ console.error('Send failed:', receipt.errorMessages.join(', '));
83
+ }
84
+ ~~~~
85
+
86
+
87
+ Factory Function
88
+ ----------------
89
+
90
+ For simplified setup, use the factory function:
91
+
92
+ ~~~~ typescript
93
+ import { createOpenTelemetryTransport } from "@upyo/opentelemetry";
94
+
95
+ const transport = createOpenTelemetryTransport(baseTransport, {
96
+ serviceName: "email-service",
97
+ serviceVersion: "1.0.0",
98
+ metrics: { prefix: "myapp" },
99
+ tracing: { recordSensitiveData: false }
100
+ });
101
+ ~~~~
102
+
103
+
104
+ Auto-Configuration
105
+ ------------------
106
+
107
+ For environments where you want automatic OpenTelemetry setup:
108
+
109
+ ~~~~ typescript
110
+ import { createOpenTelemetryTransport } from "@upyo/opentelemetry";
111
+
112
+ const transport = createOpenTelemetryTransport(baseTransport, {
113
+ serviceName: "email-service",
114
+ auto: {
115
+ tracing: { endpoint: "http://jaeger:14268/api/traces" },
116
+ metrics: { endpoint: "http://prometheus:9090/api/v1/write" }
117
+ }
118
+ });
119
+ ~~~~
120
+
121
+
122
+ Resource Management
123
+ -------------------
124
+
125
+ The OpenTelemetryTransport implements `AsyncDisposable` and automatically
126
+ handles cleanup of wrapped transports:
127
+
128
+ ~~~~ typescript
129
+ // Using with explicit disposal
130
+ const transport = new OpenTelemetryTransport(smtpTransport, config);
131
+ try {
132
+ await transport.send(message);
133
+ } finally {
134
+ await transport[Symbol.asyncDispose]();
135
+ }
136
+
137
+ // Using with 'using' statement (when available)
138
+ {
139
+ using transport = new OpenTelemetryTransport(smtpTransport, config);
140
+ await transport.send(message);
141
+ // Automatically disposed at end of scope
142
+ }
143
+ ~~~~
144
+
145
+ **Disposal Priority:**
146
+
147
+ 1. If wrapped transport supports `AsyncDisposable` → calls `[Symbol.asyncDispose]()`
148
+ 2. If wrapped transport supports `Disposable` → calls `[Symbol.dispose]()`
149
+ 3. If wrapped transport supports neither → no-op (no error thrown)
150
+
151
+
152
+ Configuration
153
+ -------------
154
+
155
+ ### `OpenTelemetryConfig`
156
+
157
+ | Option | Type | Default | Description |
158
+ | -------------------- | ---------------- | ------------------------ | ----------------------------------------------------------- |
159
+ | `tracerProvider` | `TracerProvider` | | OpenTelemetry tracer provider (required if tracing enabled) |
160
+ | `meterProvider` | `MeterProvider` | | OpenTelemetry meter provider (required if metrics enabled) |
161
+ | `metrics` | `MetricsConfig` | `{ enabled: true }` | Metrics collection configuration |
162
+ | `tracing` | `TracingConfig` | `{ enabled: true }` | Tracing configuration |
163
+ | `attributeExtractor` | `Function` | | Custom attribute extractor function |
164
+ | `errorClassifier` | `Function` | `defaultErrorClassifier` | Custom error classifier function |
165
+ | `auto` | `AutoConfig` | | Auto-configuration options |
166
+
167
+ ### `MetricsConfig`
168
+
169
+ | Option | Type | Default | Description |
170
+ | ----------------- | ---------- | --------------------- | -------------------------------------- |
171
+ | `enabled` | `boolean` | `true` | Whether metrics collection is enabled |
172
+ | `samplingRate` | `number` | `1.0` | Sampling rate (0.0 to 1.0) |
173
+ | `prefix` | `string` | `"upyo"` | Prefix for metric names |
174
+ | `durationBuckets` | `number[]` | `[0.001, 0.005, ...]` | Histogram buckets for duration metrics |
175
+
176
+ ### `TracingConfig`
177
+
178
+ | Option | Type | Default | Description |
179
+ | --------------------- | --------- | --------- | ---------------------------------------------- |
180
+ | `enabled` | `boolean` | `true` | Whether tracing is enabled |
181
+ | `samplingRate` | `number` | `1.0` | Sampling rate (0.0 to 1.0) |
182
+ | `recordSensitiveData` | `boolean` | `false` | Whether to record email addresses and subjects |
183
+ | `spanPrefix` | `string` | `"email"` | Prefix for span names |
184
+
185
+
186
+ Telemetry Data
187
+ --------------
188
+
189
+ ### Metrics
190
+
191
+ The transport collects the following metrics:
192
+
193
+ | Metric Name | Type | Labels | Description |
194
+ | ------------------------------- | --------- | ----------------------------------- | -------------------------------- |
195
+ | `upyo_email_attempts_total` | Counter | `transport`, `status`, `error_type` | Total email send attempts |
196
+ | `upyo_email_messages_total` | Counter | `transport`, `priority` | Total email messages processed |
197
+ | `upyo_email_duration_seconds` | Histogram | `transport`, `operation` | Duration of email operations |
198
+ | `upyo_email_message_size_bytes` | Histogram | `transport`, `content_type` | Size of email messages |
199
+ | `upyo_email_attachments_count` | Histogram | `transport` | Number of attachments per email |
200
+ | `upyo_email_active_sends` | Gauge | `transport` | Currently active send operations |
201
+
202
+ ### Span Attributes
203
+
204
+ The transport creates spans with the following attributes:
205
+
206
+ #### General Operation Attributes
207
+
208
+ | Attribute | Type | Description |
209
+ | ---------------- | -------- | ------------------------------------------------- |
210
+ | `operation.name` | `string` | Operation name (`email.send`, `email.send_batch`) |
211
+ | `operation.type` | `string` | Always `"email"` |
212
+
213
+ #### Network Attributes
214
+
215
+ | Attribute | Type | Description |
216
+ | ----------------------- | -------- | ---------------------------------- |
217
+ | `network.protocol.name` | `string` | Protocol (`smtp`, `https`, `http`) |
218
+ | `server.address` | `string` | Server hostname |
219
+ | `server.port` | `number` | Server port |
220
+
221
+ #### Email-Specific Attributes
222
+
223
+ | Attribute | Type | Description |
224
+ | ------------------------- | -------- | ----------------------------------------------- |
225
+ | `email.from.address` | `string` | Sender email (if `recordSensitiveData: true`) |
226
+ | `email.from.domain` | `string` | Sender domain (if `recordSensitiveData: false`) |
227
+ | `email.to.count` | `number` | Number of recipients |
228
+ | `email.cc.count` | `number` | Number of CC recipients |
229
+ | `email.bcc.count` | `number` | Number of BCC recipients |
230
+ | `email.subject.length` | `number` | Length of subject line |
231
+ | `email.attachments.count` | `number` | Number of attachments |
232
+ | `email.message.size` | `number` | Estimated message size in bytes |
233
+ | `email.content.type` | `string` | Content type (`html`, `text`, `multipart`) |
234
+ | `email.priority` | `string` | Message priority (`high`, `normal`, `low`) |
235
+ | `email.tags` | `string` | JSON array of message tags |
236
+
237
+ #### Upyo-Specific Attributes
238
+
239
+ | Attribute | Type | Description |
240
+ | ------------------------ | -------- | ---------------------------------------- |
241
+ | `upyo.transport.name` | `string` | Transport type (`mailgun`, `smtp`, etc.) |
242
+ | `upyo.transport.version` | `string` | Transport version |
243
+ | `upyo.message.id` | `string` | Message ID from successful sends |
244
+ | `upyo.retry.count` | `number` | Number of retry attempts |
245
+ | `upyo.batch.size` | `number` | Batch size for batch operations |
246
+
247
+ ### Span Events
248
+
249
+ | Event Name | Description | Attributes |
250
+ | ------------------- | ------------------------- | ------------------------------------- |
251
+ | `email.sent` | Email successfully sent | `message.id` |
252
+ | `email.send_failed` | Email send failed | `error.count`, `error.messages` |
253
+ | `exception` | Exception occurred | `exception.type`, `exception.message` |
254
+ | `retry` | Retry attempt | `retry.attempt`, `retry.reason` |
255
+ | `partial_success` | Batch partially succeeded | `success_count`, `failure_count` |
256
+
257
+
258
+ Error Classification
259
+ --------------------
260
+
261
+ The transport automatically classifies errors into standard categories:
262
+
263
+ | Category | Description | Example Errors |
264
+ | --------------------- | ----------------------- | --------------------------------------- |
265
+ | `auth` | Authentication failures | Invalid API key, unauthorized |
266
+ | `rate_limit` | Rate limiting | Quota exceeded, too many requests |
267
+ | `network` | Network connectivity | Timeout, DNS resolution, abort |
268
+ | `validation` | Input validation | Invalid email format, malformed request |
269
+ | `service_unavailable` | Service outages | HTTP 503, temporarily unavailable |
270
+ | `server_error` | Server errors | HTTP 500, internal server error |
271
+ | `unknown` | Unclassified errors | Any other errors |
272
+
273
+ ### Custom Error Classification
274
+
275
+ You can provide a custom error classifier:
276
+
277
+ ~~~~ typescript
278
+ import { createErrorClassifier } from "@upyo/opentelemetry";
279
+
280
+ const customClassifier = createErrorClassifier({
281
+ patterns: {
282
+ "spam": /spam|blocked|reputation/i,
283
+ "bounce": /bounce|undeliverable|invalid.*recipient/i,
284
+ },
285
+ fallback: "email_error"
286
+ });
287
+
288
+ const transport = new OpenTelemetryTransport(baseTransport, {
289
+ errorClassifier: customClassifier,
290
+ // ... other config
291
+ });
292
+ ~~~~
293
+
294
+
295
+ Custom Attributes
296
+ -----------------
297
+
298
+ Add custom attributes to spans and metrics:
299
+
300
+ ~~~~ typescript
301
+ import { createEmailAttributeExtractor } from "@upyo/opentelemetry";
302
+
303
+ const customExtractor = createEmailAttributeExtractor("my-transport", {
304
+ customAttributes: (operation, transportName, messageCount, totalSize) => ({
305
+ "app.version": "1.0.0",
306
+ "app.environment": process.env.NODE_ENV,
307
+ "deployment.id": process.env.DEPLOYMENT_ID,
308
+ })
309
+ });
310
+
311
+ const transport = new OpenTelemetryTransport(baseTransport, {
312
+ attributeExtractor: customExtractor,
313
+ // ... other config
314
+ });
315
+ ~~~~
316
+
317
+
318
+ Performance Considerations
319
+ --------------------------
320
+
321
+ ### Sampling
322
+
323
+ Control overhead with sampling rates:
324
+
325
+ ~~~~ typescript
326
+ const transport = new OpenTelemetryTransport(baseTransport, {
327
+ metrics: {
328
+ enabled: true,
329
+ samplingRate: 0.1 // Sample 10% of operations
330
+ },
331
+ tracing: {
332
+ enabled: true,
333
+ samplingRate: 0.05 // Sample 5% of operations
334
+ }
335
+ });
336
+ ~~~~
337
+
338
+ ### Feature Toggles
339
+
340
+ Disable features selectively:
341
+
342
+ ~~~~ typescript
343
+ // Metrics only, no tracing
344
+ const metricsOnlyTransport = new OpenTelemetryTransport(baseTransport, {
345
+ meterProvider: metrics.getMeterProvider(),
346
+ metrics: { enabled: true },
347
+ tracing: { enabled: false }
348
+ });
349
+
350
+ // Tracing only, no metrics
351
+ const tracingOnlyTransport = new OpenTelemetryTransport(baseTransport, {
352
+ tracerProvider: trace.getTracerProvider(),
353
+ tracing: { enabled: true },
354
+ metrics: { enabled: false }
355
+ });
356
+ ~~~~
357
+
358
+
359
+ Monitoring Examples
360
+ -------------------
361
+
362
+ ### Prometheus Queries
363
+
364
+ ~~~~ promql
365
+ # Email delivery success rate
366
+ rate(upyo_email_attempts_total{status="success"}[5m]) /
367
+ rate(upyo_email_attempts_total[5m])
368
+
369
+ # Average email send duration
370
+ rate(upyo_email_duration_seconds_sum[5m]) /
371
+ rate(upyo_email_duration_seconds_count[5m])
372
+
373
+ # Error rate by transport
374
+ rate(upyo_email_attempts_total{status="failure"}[5m]) by (transport, error_type)
375
+
376
+ # Active email sending operations
377
+ upyo_email_active_sends
378
+ ~~~~
379
+
380
+ ### Grafana Dashboard Panels
381
+
382
+ ~~~~ json
383
+ {
384
+ "title": "Email Delivery Success Rate",
385
+ "type": "stat",
386
+ "targets": [{
387
+ "expr": "rate(upyo_email_attempts_total{status=\"success\"}[5m]) / rate(upyo_email_attempts_total[5m])",
388
+ "format": "time_series"
389
+ }]
390
+ }
391
+ ~~~~
392
+
393
+ ### Alerting Rules
394
+
395
+ ~~~~ yaml
396
+ groups:
397
+ - name: email_alerts
398
+ rules:
399
+ - alert: EmailDeliveryFailureRate
400
+ expr: |
401
+ (
402
+ rate(upyo_email_attempts_total{status="failure"}[5m]) /
403
+ rate(upyo_email_attempts_total[5m])
404
+ ) > 0.05
405
+ for: 2m
406
+ labels:
407
+ severity: warning
408
+ annotations:
409
+ summary: "High email delivery failure rate"
410
+ description: "Email failure rate is {{ $value | humanizePercentage }} for transport {{ $labels.transport }}"
411
+
412
+ - alert: EmailSendDurationHigh
413
+ expr: |
414
+ histogram_quantile(0.95, rate(upyo_email_duration_seconds_bucket[5m])) > 10
415
+ for: 5m
416
+ labels:
417
+ severity: warning
418
+ annotations:
419
+ summary: "High email send duration"
420
+ description: "95th percentile email send duration is {{ $value }}s"
421
+ ~~~~
422
+
423
+
424
+ Cross-Runtime Compatibility
425
+ ---------------------------
426
+
427
+ This package works across all JavaScript runtimes:
428
+
429
+ - **Node.js**: Full OpenTelemetry ecosystem support
430
+ - **Deno**: Native ESM and web standards compatibility
431
+ - **Bun**: High-performance runtime optimizations
432
+ - **Edge functions**: Minimal overhead for serverless environments
433
+
434
+ Resource cleanup is handled automatically via `AsyncDisposable` when supported:
435
+
436
+ ~~~~ typescript
437
+ // Automatic cleanup with using statement (Node.js 20+, modern browsers)
438
+ await using transport = new OpenTelemetryTransport(baseTransport, config);
439
+ await transport.send(message);
440
+ // Transport is automatically disposed here
441
+
442
+ // Manual cleanup for older environments
443
+ const transport = new OpenTelemetryTransport(baseTransport, config);
444
+ try {
445
+ await transport.send(message);
446
+ } finally {
447
+ await transport[Symbol.asyncDispose]();
448
+ }
449
+ ~~~~