@xenterprises/fastify-xlogger 1.0.0 → 1.1.1

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 CHANGED
@@ -226,6 +226,91 @@ const options = getLoggerOptions();
226
226
  const options = getLoggerOptions({ pretty: true });
227
227
  ```
228
228
 
229
+ ### 7. Custom Transports (Betterstack, Logtail, etc.)
230
+
231
+ Send logs to centralized logging services like Betterstack/Logtail using custom transports:
232
+
233
+ #### Install Transport (Optional Peer Dependency)
234
+
235
+ ```bash
236
+ npm install @logtail/pino
237
+ ```
238
+
239
+ #### Single Transport (Betterstack)
240
+
241
+ ```javascript
242
+ import Fastify from "fastify";
243
+ import xLogger, { getLoggerOptions } from "xlogger";
244
+
245
+ const fastify = Fastify({
246
+ logger: getLoggerOptions({
247
+ serviceName: "my-api",
248
+ transport: {
249
+ target: "@logtail/pino",
250
+ options: {
251
+ sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN,
252
+ },
253
+ },
254
+ }),
255
+ });
256
+
257
+ await fastify.register(xLogger, {
258
+ serviceName: "my-api",
259
+ });
260
+ ```
261
+
262
+ #### Multiple Transports
263
+
264
+ Send logs to both Betterstack and a local file:
265
+
266
+ ```javascript
267
+ const fastify = Fastify({
268
+ logger: getLoggerOptions({
269
+ serviceName: "my-api",
270
+ transport: {
271
+ targets: [
272
+ {
273
+ target: "@logtail/pino",
274
+ options: {
275
+ sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN,
276
+ },
277
+ },
278
+ {
279
+ target: "pino/file",
280
+ options: {
281
+ destination: "/var/log/app.log",
282
+ },
283
+ },
284
+ ],
285
+ },
286
+ }),
287
+ });
288
+ ```
289
+
290
+ #### Environment-Based Configuration
291
+
292
+ ```javascript
293
+ // In development: use pino-pretty (default)
294
+ // In production: use Betterstack if token is set, otherwise JSON stdout
295
+ const transport = process.env.BETTERSTACK_SOURCE_TOKEN
296
+ ? {
297
+ target: "@logtail/pino",
298
+ options: {
299
+ sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN,
300
+ },
301
+ }
302
+ : undefined;
303
+
304
+ const fastify = Fastify({
305
+ logger: getLoggerOptions({
306
+ serviceName: "my-api",
307
+ transport,
308
+ }),
309
+ });
310
+ ```
311
+
312
+ **Note**: When a custom transport is provided, it overrides the default environment-based transport configuration (pino-pretty in development, JSON in production).
313
+
229
314
  ## API Reference
230
315
 
231
316
  ### Decorators
@@ -320,6 +405,12 @@ const fastify = Fastify({
320
405
  serviceName: "my-api", // Optional: service name
321
406
  redactPaths: ["custom"], // Optional: additional redact paths
322
407
  pretty: false, // Optional: force pretty printing
408
+ transport: { // Optional: custom transport configuration
409
+ target: "@logtail/pino",
410
+ options: {
411
+ sourceToken: process.env.BETTERSTACK_SOURCE_TOKEN,
412
+ },
413
+ },
323
414
  }),
324
415
  });
325
416
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenterprises/fastify-xlogger",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Fastify plugin for standardized logging with Pino - context, redaction, and canonical schema",
5
5
  "type": "module",
6
6
  "main": "src/xLogger.js",
@@ -30,7 +30,13 @@
30
30
  "fastify-plugin": "^5.0.1"
31
31
  },
32
32
  "peerDependencies": {
33
- "fastify": "^5.0.0"
33
+ "fastify": "^5.0.0",
34
+ "@logtail/pino": "^0.5.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "@logtail/pino": {
38
+ "optional": true
39
+ }
34
40
  },
35
41
  "devDependencies": {
36
42
  "eslint": "^9.0.0",
package/src/xLogger.js CHANGED
@@ -399,6 +399,10 @@ export default fp(xLogger, {
399
399
  * @param {string[]} [options.redactPaths] - Additional paths to redact
400
400
  * @param {boolean} [options.pretty] - Force pretty printing
401
401
  * @param {string} [options.serviceName] - Service name
402
+ * @param {Object} [options.transport] - Custom Pino transport configuration
403
+ * @param {string} [options.transport.target] - Transport target module (e.g., '@logtail/pino')
404
+ * @param {Object} [options.transport.options] - Transport options
405
+ * @param {Array} [options.transport.targets] - Multiple transport targets
402
406
  * @returns {Object} Pino logger options
403
407
  */
404
408
  export function getLoggerOptions(options = {}) {
@@ -444,8 +448,12 @@ export function getLoggerOptions(options = {}) {
444
448
  },
445
449
  };
446
450
 
447
- // Pretty print in development
448
- if (!isProd || options.pretty) {
451
+ // If custom transport is provided, use it
452
+ if (options.transport) {
453
+ loggerOptions.transport = options.transport;
454
+ }
455
+ // Otherwise, use pretty print in development
456
+ else if (!isProd || options.pretty) {
449
457
  loggerOptions.transport = {
450
458
  target: "pino-pretty",
451
459
  options: {
@@ -1,402 +0,0 @@
1
- /**
2
- * xLogger Tests
3
- *
4
- * Tests for the xLogger Fastify plugin
5
- */
6
-
7
- import { describe, test, beforeEach, afterEach } from "node:test";
8
- import assert from "node:assert";
9
- import Fastify from "fastify";
10
- import xLogger, { getLoggerOptions, DEFAULT_REDACT_PATHS, LOG_LEVELS } from "../src/xLogger.js";
11
-
12
- describe("xLogger Plugin", () => {
13
- let fastify;
14
-
15
- beforeEach(() => {
16
- fastify = Fastify({
17
- logger: {
18
- level: "silent", // Suppress logs during tests
19
- },
20
- });
21
- });
22
-
23
- afterEach(async () => {
24
- await fastify.close();
25
- });
26
-
27
- describe("Plugin Registration", () => {
28
- test("should register successfully", async () => {
29
- await fastify.register(xLogger, {});
30
- await fastify.ready();
31
-
32
- assert.ok(fastify.xlogger, "xlogger should exist");
33
- assert.ok(fastify.xlogger.config, "xlogger.config should exist");
34
- assert.ok(fastify.xlogger.logEvent, "xlogger.logEvent should exist");
35
- assert.ok(fastify.xlogger.logBoundary, "xlogger.logBoundary should exist");
36
- assert.ok(fastify.xlogger.createBoundaryLogger, "xlogger.createBoundaryLogger should exist");
37
- assert.ok(fastify.xlogger.createJobContext, "xlogger.createJobContext should exist");
38
- assert.ok(fastify.xlogger.extractContext, "xlogger.extractContext should exist");
39
- });
40
-
41
- test("should skip registration when active is false", async () => {
42
- await fastify.register(xLogger, { active: false });
43
- await fastify.ready();
44
-
45
- assert.ok(!fastify.xlogger, "xlogger should not exist");
46
- });
47
-
48
- test("should use default redact paths", async () => {
49
- await fastify.register(xLogger, {});
50
- await fastify.ready();
51
-
52
- assert.ok(fastify.xlogger.redactPaths.includes("password"));
53
- assert.ok(fastify.xlogger.redactPaths.includes("token"));
54
- assert.ok(fastify.xlogger.redactPaths.includes("req.headers.authorization"));
55
- });
56
-
57
- test("should extend redact paths with custom paths", async () => {
58
- await fastify.register(xLogger, {
59
- redactPaths: ["customSecret", "myApiKey"],
60
- });
61
- await fastify.ready();
62
-
63
- assert.ok(fastify.xlogger.redactPaths.includes("password"));
64
- assert.ok(fastify.xlogger.redactPaths.includes("customSecret"));
65
- assert.ok(fastify.xlogger.redactPaths.includes("myApiKey"));
66
- });
67
-
68
- test("should replace redact paths when redactClobber is true", async () => {
69
- await fastify.register(xLogger, {
70
- redactPaths: ["onlyThis"],
71
- redactClobber: true,
72
- });
73
- await fastify.ready();
74
-
75
- assert.ok(!fastify.xlogger.redactPaths.includes("password"));
76
- assert.ok(fastify.xlogger.redactPaths.includes("onlyThis"));
77
- });
78
-
79
- test("should store service name in config", async () => {
80
- await fastify.register(xLogger, {
81
- serviceName: "my-test-service",
82
- });
83
- await fastify.ready();
84
-
85
- assert.strictEqual(fastify.xlogger.config.serviceName, "my-test-service");
86
- });
87
- });
88
-
89
- describe("Request Context", () => {
90
- test("should decorate request with contextLog", async () => {
91
- await fastify.register(xLogger, {});
92
-
93
- fastify.get("/test", async (request) => {
94
- assert.ok(request.contextLog, "contextLog should exist on request");
95
- return { ok: true };
96
- });
97
-
98
- await fastify.ready();
99
-
100
- const response = await fastify.inject({
101
- method: "GET",
102
- url: "/test",
103
- });
104
-
105
- assert.strictEqual(response.statusCode, 200);
106
- });
107
-
108
- test("should extract context from request", async () => {
109
- await fastify.register(xLogger, {});
110
- let extractedContext;
111
-
112
- fastify.get("/test", async (request) => {
113
- extractedContext = fastify.xlogger.extractContext(request);
114
- return { ok: true };
115
- });
116
-
117
- await fastify.ready();
118
-
119
- await fastify.inject({
120
- method: "GET",
121
- url: "/test",
122
- });
123
-
124
- assert.ok(extractedContext.requestId, "requestId should exist");
125
- assert.strictEqual(extractedContext.method, "GET");
126
- assert.strictEqual(extractedContext.route, "/test");
127
- });
128
-
129
- test("should extract orgId from x-org-id header", async () => {
130
- await fastify.register(xLogger, {});
131
- let extractedContext;
132
-
133
- fastify.get("/test", async (request) => {
134
- extractedContext = fastify.xlogger.extractContext(request);
135
- return { ok: true };
136
- });
137
-
138
- await fastify.ready();
139
-
140
- await fastify.inject({
141
- method: "GET",
142
- url: "/test",
143
- headers: {
144
- "x-org-id": "org_123",
145
- },
146
- });
147
-
148
- assert.strictEqual(extractedContext.orgId, "org_123");
149
- });
150
-
151
- test("should extract userId from x-user-id header", async () => {
152
- await fastify.register(xLogger, {});
153
- let extractedContext;
154
-
155
- fastify.get("/test", async (request) => {
156
- extractedContext = fastify.xlogger.extractContext(request);
157
- return { ok: true };
158
- });
159
-
160
- await fastify.ready();
161
-
162
- await fastify.inject({
163
- method: "GET",
164
- url: "/test",
165
- headers: {
166
- "x-user-id": "user_456",
167
- },
168
- });
169
-
170
- assert.strictEqual(extractedContext.userId, "user_456");
171
- });
172
-
173
- test("should extract OpenTelemetry trace context", async () => {
174
- await fastify.register(xLogger, {});
175
- let extractedContext;
176
-
177
- fastify.get("/test", async (request) => {
178
- extractedContext = fastify.xlogger.extractContext(request);
179
- return { ok: true };
180
- });
181
-
182
- await fastify.ready();
183
-
184
- await fastify.inject({
185
- method: "GET",
186
- url: "/test",
187
- headers: {
188
- traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
189
- },
190
- });
191
-
192
- assert.strictEqual(extractedContext.traceId, "0af7651916cd43dd8448eb211c80319c");
193
- assert.strictEqual(extractedContext.spanId, "b7ad6b7169203331");
194
- });
195
-
196
- test("should use custom context extractor", async () => {
197
- await fastify.register(xLogger, {
198
- contextExtractor: (request) => ({
199
- customField: "custom_value",
200
- fromQuery: request.query.foo,
201
- }),
202
- });
203
- let extractedContext;
204
-
205
- fastify.get("/test", async (request) => {
206
- extractedContext = fastify.xlogger.extractContext(request);
207
- return { ok: true };
208
- });
209
-
210
- await fastify.ready();
211
-
212
- await fastify.inject({
213
- method: "GET",
214
- url: "/test?foo=bar",
215
- });
216
-
217
- assert.strictEqual(extractedContext.customField, "custom_value");
218
- assert.strictEqual(extractedContext.fromQuery, "bar");
219
- });
220
- });
221
-
222
- describe("Boundary Logging", () => {
223
- test("should create boundary logger with timing", async () => {
224
- await fastify.register(xLogger, {});
225
- await fastify.ready();
226
-
227
- const boundary = fastify.xlogger.createBoundaryLogger("stripe", "createCustomer");
228
-
229
- assert.ok(boundary.success, "success method should exist");
230
- assert.ok(boundary.fail, "fail method should exist");
231
- assert.ok(boundary.retry, "retry method should exist");
232
- });
233
-
234
- test("should track retry count", async () => {
235
- await fastify.register(xLogger, {});
236
- await fastify.ready();
237
-
238
- const boundary = fastify.xlogger.createBoundaryLogger("stripe", "createCustomer");
239
-
240
- boundary.retry();
241
- boundary.retry();
242
-
243
- // The retry count is internal, but we can verify it doesn't throw
244
- assert.doesNotThrow(() => boundary.success());
245
- });
246
- });
247
-
248
- describe("Job Context", () => {
249
- test("should create job context with correlation ID", async () => {
250
- await fastify.register(xLogger, {});
251
- await fastify.ready();
252
-
253
- const job = fastify.xlogger.createJobContext({
254
- jobName: "processPayments",
255
- orgId: "org_123",
256
- userId: "user_456",
257
- });
258
-
259
- assert.ok(job.context, "context should exist");
260
- assert.ok(job.log, "log should exist");
261
- assert.ok(job.start, "start method should exist");
262
- assert.ok(job.complete, "complete method should exist");
263
- assert.ok(job.fail, "fail method should exist");
264
- assert.ok(job.context.correlationId, "correlationId should be generated");
265
- assert.strictEqual(job.context.jobName, "processPayments");
266
- assert.strictEqual(job.context.orgId, "org_123");
267
- assert.strictEqual(job.context.userId, "user_456");
268
- });
269
-
270
- test("should use provided correlation ID", async () => {
271
- await fastify.register(xLogger, {});
272
- await fastify.ready();
273
-
274
- const job = fastify.xlogger.createJobContext({
275
- jobName: "syncData",
276
- correlationId: "custom_corr_123",
277
- });
278
-
279
- assert.strictEqual(job.context.correlationId, "custom_corr_123");
280
- });
281
-
282
- test("should include original request ID", async () => {
283
- await fastify.register(xLogger, {});
284
- await fastify.ready();
285
-
286
- const job = fastify.xlogger.createJobContext({
287
- jobName: "asyncTask",
288
- requestId: "req_789",
289
- });
290
-
291
- assert.strictEqual(job.context.originalRequestId, "req_789");
292
- });
293
- });
294
-
295
- describe("Log Event", () => {
296
- test("should log event without request", async () => {
297
- await fastify.register(xLogger, {});
298
- await fastify.ready();
299
-
300
- // Should not throw
301
- assert.doesNotThrow(() => {
302
- fastify.xlogger.logEvent({
303
- event: "user.created",
304
- msg: "User was created",
305
- data: { email: "test@example.com" },
306
- });
307
- });
308
- });
309
-
310
- test("should log event with different levels", async () => {
311
- await fastify.register(xLogger, {});
312
- await fastify.ready();
313
-
314
- // Should not throw for different levels
315
- assert.doesNotThrow(() => {
316
- fastify.xlogger.logEvent({
317
- event: "debug.event",
318
- level: "debug",
319
- });
320
- });
321
-
322
- assert.doesNotThrow(() => {
323
- fastify.xlogger.logEvent({
324
- event: "warn.event",
325
- level: "warn",
326
- });
327
- });
328
-
329
- assert.doesNotThrow(() => {
330
- fastify.xlogger.logEvent({
331
- event: "error.event",
332
- level: "error",
333
- });
334
- });
335
- });
336
- });
337
- });
338
-
339
- describe("getLoggerOptions", () => {
340
- test("should return valid Pino options", () => {
341
- const options = getLoggerOptions();
342
-
343
- assert.ok(options.level, "level should exist");
344
- assert.ok(options.redact, "redact should exist");
345
- assert.ok(options.serializers, "serializers should exist");
346
- assert.ok(options.base, "base should exist");
347
- });
348
-
349
- test("should use debug level in non-production", () => {
350
- const originalEnv = process.env.NODE_ENV;
351
- process.env.NODE_ENV = "development";
352
-
353
- const options = getLoggerOptions();
354
-
355
- assert.strictEqual(options.level, "debug");
356
-
357
- process.env.NODE_ENV = originalEnv;
358
- });
359
-
360
- test("should include pretty transport in non-production", () => {
361
- const originalEnv = process.env.NODE_ENV;
362
- process.env.NODE_ENV = "development";
363
-
364
- const options = getLoggerOptions();
365
-
366
- assert.ok(options.transport, "transport should exist");
367
- assert.strictEqual(options.transport.target, "pino-pretty");
368
-
369
- process.env.NODE_ENV = originalEnv;
370
- });
371
-
372
- test("should allow custom service name", () => {
373
- const options = getLoggerOptions({ serviceName: "my-service" });
374
-
375
- assert.strictEqual(options.base.service, "my-service");
376
- });
377
-
378
- test("should extend redact paths", () => {
379
- const options = getLoggerOptions({ redactPaths: ["customPath"] });
380
-
381
- assert.ok(options.redact.paths.includes("customPath"));
382
- assert.ok(options.redact.paths.includes("password"));
383
- });
384
- });
385
-
386
- describe("Exports", () => {
387
- test("should export DEFAULT_REDACT_PATHS", () => {
388
- assert.ok(Array.isArray(DEFAULT_REDACT_PATHS));
389
- assert.ok(DEFAULT_REDACT_PATHS.length > 0);
390
- assert.ok(DEFAULT_REDACT_PATHS.includes("password"));
391
- });
392
-
393
- test("should export LOG_LEVELS", () => {
394
- assert.ok(LOG_LEVELS);
395
- assert.strictEqual(LOG_LEVELS.fatal, 60);
396
- assert.strictEqual(LOG_LEVELS.error, 50);
397
- assert.strictEqual(LOG_LEVELS.warn, 40);
398
- assert.strictEqual(LOG_LEVELS.info, 30);
399
- assert.strictEqual(LOG_LEVELS.debug, 20);
400
- assert.strictEqual(LOG_LEVELS.trace, 10);
401
- });
402
- });