@logtape/logtape 1.0.0-dev.246 → 1.0.0-dev.247

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/formatter.ts CHANGED
@@ -209,6 +209,130 @@ export interface TextFormatterOptions {
209
209
  format?: (values: FormattedValues) => string;
210
210
  }
211
211
 
212
+ // Optimized helper functions for timestamp formatting
213
+ function padZero(num: number): string {
214
+ return num < 10 ? `0${num}` : `${num}`;
215
+ }
216
+
217
+ function padThree(num: number): string {
218
+ return num < 10 ? `00${num}` : num < 100 ? `0${num}` : `${num}`;
219
+ }
220
+
221
+ // Pre-optimized timestamp formatter functions
222
+ const timestampFormatters = {
223
+ "date-time-timezone": (ts: number): string => {
224
+ const d = new Date(ts);
225
+ const year = d.getUTCFullYear();
226
+ const month = padZero(d.getUTCMonth() + 1);
227
+ const day = padZero(d.getUTCDate());
228
+ const hour = padZero(d.getUTCHours());
229
+ const minute = padZero(d.getUTCMinutes());
230
+ const second = padZero(d.getUTCSeconds());
231
+ const ms = padThree(d.getUTCMilliseconds());
232
+ return `${year}-${month}-${day} ${hour}:${minute}:${second}.${ms} +00:00`;
233
+ },
234
+ "date-time-tz": (ts: number): string => {
235
+ const d = new Date(ts);
236
+ const year = d.getUTCFullYear();
237
+ const month = padZero(d.getUTCMonth() + 1);
238
+ const day = padZero(d.getUTCDate());
239
+ const hour = padZero(d.getUTCHours());
240
+ const minute = padZero(d.getUTCMinutes());
241
+ const second = padZero(d.getUTCSeconds());
242
+ const ms = padThree(d.getUTCMilliseconds());
243
+ return `${year}-${month}-${day} ${hour}:${minute}:${second}.${ms} +00`;
244
+ },
245
+ "date-time": (ts: number): string => {
246
+ const d = new Date(ts);
247
+ const year = d.getUTCFullYear();
248
+ const month = padZero(d.getUTCMonth() + 1);
249
+ const day = padZero(d.getUTCDate());
250
+ const hour = padZero(d.getUTCHours());
251
+ const minute = padZero(d.getUTCMinutes());
252
+ const second = padZero(d.getUTCSeconds());
253
+ const ms = padThree(d.getUTCMilliseconds());
254
+ return `${year}-${month}-${day} ${hour}:${minute}:${second}.${ms}`;
255
+ },
256
+ "time-timezone": (ts: number): string => {
257
+ const d = new Date(ts);
258
+ const hour = padZero(d.getUTCHours());
259
+ const minute = padZero(d.getUTCMinutes());
260
+ const second = padZero(d.getUTCSeconds());
261
+ const ms = padThree(d.getUTCMilliseconds());
262
+ return `${hour}:${minute}:${second}.${ms} +00:00`;
263
+ },
264
+ "time-tz": (ts: number): string => {
265
+ const d = new Date(ts);
266
+ const hour = padZero(d.getUTCHours());
267
+ const minute = padZero(d.getUTCMinutes());
268
+ const second = padZero(d.getUTCSeconds());
269
+ const ms = padThree(d.getUTCMilliseconds());
270
+ return `${hour}:${minute}:${second}.${ms} +00`;
271
+ },
272
+ "time": (ts: number): string => {
273
+ const d = new Date(ts);
274
+ const hour = padZero(d.getUTCHours());
275
+ const minute = padZero(d.getUTCMinutes());
276
+ const second = padZero(d.getUTCSeconds());
277
+ const ms = padThree(d.getUTCMilliseconds());
278
+ return `${hour}:${minute}:${second}.${ms}`;
279
+ },
280
+ "date": (ts: number): string => {
281
+ const d = new Date(ts);
282
+ const year = d.getUTCFullYear();
283
+ const month = padZero(d.getUTCMonth() + 1);
284
+ const day = padZero(d.getUTCDate());
285
+ return `${year}-${month}-${day}`;
286
+ },
287
+ "rfc3339": (ts: number): string => new Date(ts).toISOString(),
288
+ "none": (): null => null,
289
+ } as const;
290
+
291
+ // Pre-computed level renderers for common cases
292
+ const levelRenderersCache = {
293
+ ABBR: levelAbbreviations,
294
+ abbr: {
295
+ trace: "trc",
296
+ debug: "dbg",
297
+ info: "inf",
298
+ warning: "wrn",
299
+ error: "err",
300
+ fatal: "ftl",
301
+ } as const,
302
+ FULL: {
303
+ trace: "TRACE",
304
+ debug: "DEBUG",
305
+ info: "INFO",
306
+ warning: "WARNING",
307
+ error: "ERROR",
308
+ fatal: "FATAL",
309
+ } as const,
310
+ full: {
311
+ trace: "trace",
312
+ debug: "debug",
313
+ info: "info",
314
+ warning: "warning",
315
+ error: "error",
316
+ fatal: "fatal",
317
+ } as const,
318
+ L: {
319
+ trace: "T",
320
+ debug: "D",
321
+ info: "I",
322
+ warning: "W",
323
+ error: "E",
324
+ fatal: "F",
325
+ } as const,
326
+ l: {
327
+ trace: "t",
328
+ debug: "d",
329
+ info: "i",
330
+ warning: "w",
331
+ error: "e",
332
+ fatal: "f",
333
+ } as const,
334
+ } as const;
335
+
212
336
  /**
213
337
  * Get a text formatter with the specified options. Although it's flexible
214
338
  * enough to create a custom formatter, if you want more control, you can
@@ -229,61 +353,81 @@ export interface TextFormatterOptions {
229
353
  export function getTextFormatter(
230
354
  options: TextFormatterOptions = {},
231
355
  ): TextFormatter {
232
- const timestampRenderer =
233
- options.timestamp == null || options.timestamp === "date-time-timezone"
234
- ? (ts: number): string =>
235
- new Date(ts).toISOString().replace("T", " ").replace("Z", " +00:00")
236
- : options.timestamp === "date-time-tz"
237
- ? (ts: number): string =>
238
- new Date(ts).toISOString().replace("T", " ").replace("Z", " +00")
239
- : options.timestamp === "date-time"
240
- ? (ts: number): string =>
241
- new Date(ts).toISOString().replace("T", " ").replace("Z", "")
242
- : options.timestamp === "time-timezone"
243
- ? (ts: number): string =>
244
- new Date(ts).toISOString().replace(/.*T/, "").replace("Z", " +00:00")
245
- : options.timestamp === "time-tz"
246
- ? (ts: number): string =>
247
- new Date(ts).toISOString().replace(/.*T/, "").replace("Z", " +00")
248
- : options.timestamp === "time"
249
- ? (ts: number): string =>
250
- new Date(ts).toISOString().replace(/.*T/, "").replace("Z", "")
251
- : options.timestamp === "date"
252
- ? (ts: number): string => new Date(ts).toISOString().replace(/T.*/, "")
253
- : options.timestamp === "rfc3339"
254
- ? (ts: number): string => new Date(ts).toISOString()
255
- : options.timestamp === "none" || options.timestamp === "disabled"
256
- ? () => null
257
- : options.timestamp;
356
+ // Pre-compute timestamp formatter with optimized lookup
357
+ const timestampRenderer = (() => {
358
+ const tsOption = options.timestamp;
359
+ if (tsOption == null) {
360
+ return timestampFormatters["date-time-timezone"];
361
+ } else if (tsOption === "disabled") {
362
+ return timestampFormatters["none"];
363
+ } else if (
364
+ typeof tsOption === "string" && tsOption in timestampFormatters
365
+ ) {
366
+ return timestampFormatters[tsOption as keyof typeof timestampFormatters];
367
+ } else {
368
+ return tsOption as (ts: number) => string | null;
369
+ }
370
+ })();
371
+
258
372
  const categorySeparator = options.category ?? "·";
259
373
  const valueRenderer = options.value ?? inspect;
260
- const levelRenderer = options.level == null || options.level === "ABBR"
261
- ? (level: LogLevel): string => levelAbbreviations[level]
262
- : options.level === "abbr"
263
- ? (level: LogLevel): string => levelAbbreviations[level].toLowerCase()
264
- : options.level === "FULL"
265
- ? (level: LogLevel): string => level.toUpperCase()
266
- : options.level === "full"
267
- ? (level: LogLevel): string => level
268
- : options.level === "L"
269
- ? (level: LogLevel): string => level.charAt(0).toUpperCase()
270
- : options.level === "l"
271
- ? (level: LogLevel): string => level.charAt(0)
272
- : options.level;
374
+
375
+ // Pre-compute level renderer for better performance
376
+ const levelRenderer = (() => {
377
+ const levelOption = options.level;
378
+ if (levelOption == null || levelOption === "ABBR") {
379
+ return (level: LogLevel): string => levelRenderersCache.ABBR[level];
380
+ } else if (levelOption === "abbr") {
381
+ return (level: LogLevel): string => levelRenderersCache.abbr[level];
382
+ } else if (levelOption === "FULL") {
383
+ return (level: LogLevel): string => levelRenderersCache.FULL[level];
384
+ } else if (levelOption === "full") {
385
+ return (level: LogLevel): string => levelRenderersCache.full[level];
386
+ } else if (levelOption === "L") {
387
+ return (level: LogLevel): string => levelRenderersCache.L[level];
388
+ } else if (levelOption === "l") {
389
+ return (level: LogLevel): string => levelRenderersCache.l[level];
390
+ } else {
391
+ return levelOption;
392
+ }
393
+ })();
394
+
273
395
  const formatter: (values: FormattedValues) => string = options.format ??
274
396
  (({ timestamp, level, category, message }: FormattedValues) =>
275
397
  `${timestamp ? `${timestamp} ` : ""}[${level}] ${category}: ${message}`);
398
+
276
399
  return (record: LogRecord): string => {
277
- let message = "";
278
- for (let i = 0; i < record.message.length; i++) {
279
- if (i % 2 === 0) message += record.message[i];
280
- else message += valueRenderer(record.message[i]);
400
+ // Optimized message building
401
+ const msgParts = record.message;
402
+ const msgLen = msgParts.length;
403
+
404
+ let message: string;
405
+ if (msgLen === 1) {
406
+ // Fast path for simple messages with no interpolation
407
+ message = msgParts[0] as string;
408
+ } else if (msgLen <= 6) {
409
+ // Fast path for small messages - direct concatenation
410
+ message = "";
411
+ for (let i = 0; i < msgLen; i++) {
412
+ message += (i % 2 === 0) ? msgParts[i] : valueRenderer(msgParts[i]);
413
+ }
414
+ } else {
415
+ // Optimized path for larger messages - array join
416
+ const parts: string[] = new Array(msgLen);
417
+ for (let i = 0; i < msgLen; i++) {
418
+ parts[i] = (i % 2 === 0)
419
+ ? msgParts[i] as string
420
+ : valueRenderer(msgParts[i]);
421
+ }
422
+ message = parts.join("");
281
423
  }
424
+
282
425
  const timestamp = timestampRenderer(record.timestamp);
283
426
  const level = levelRenderer(record.level);
284
427
  const category = typeof categorySeparator === "function"
285
428
  ? categorySeparator(record.category)
286
429
  : record.category.join(categorySeparator);
430
+
287
431
  const values: FormattedValues = {
288
432
  timestamp,
289
433
  level,
@@ -574,6 +718,58 @@ export interface JsonLinesFormatterOptions {
574
718
  export function getJsonLinesFormatter(
575
719
  options: JsonLinesFormatterOptions = {},
576
720
  ): TextFormatter {
721
+ // Most common configuration - optimize for the default case
722
+ if (!options.categorySeparator && !options.message && !options.properties) {
723
+ // Ultra-minimalist path - eliminate all possible overhead
724
+ return (record: LogRecord): string => {
725
+ // Direct benchmark pattern match (most common case first)
726
+ if (record.message.length === 3) {
727
+ return JSON.stringify({
728
+ "@timestamp": new Date(record.timestamp).toISOString(),
729
+ level: record.level === "warning"
730
+ ? "WARN"
731
+ : record.level.toUpperCase(),
732
+ message: record.message[0] + JSON.stringify(record.message[1]) +
733
+ record.message[2],
734
+ logger: record.category.join("."),
735
+ properties: record.properties,
736
+ });
737
+ }
738
+
739
+ // Single message (second most common)
740
+ if (record.message.length === 1) {
741
+ return JSON.stringify({
742
+ "@timestamp": new Date(record.timestamp).toISOString(),
743
+ level: record.level === "warning"
744
+ ? "WARN"
745
+ : record.level.toUpperCase(),
746
+ message: record.message[0],
747
+ logger: record.category.join("."),
748
+ properties: record.properties,
749
+ });
750
+ }
751
+
752
+ // Complex messages (fallback)
753
+ let msg = record.message[0] as string;
754
+ for (let i = 1; i < record.message.length; i++) {
755
+ msg += (i & 1) ? JSON.stringify(record.message[i]) : record.message[i];
756
+ }
757
+
758
+ return JSON.stringify({
759
+ "@timestamp": new Date(record.timestamp).toISOString(),
760
+ level: record.level === "warning" ? "WARN" : record.level.toUpperCase(),
761
+ message: msg,
762
+ logger: record.category.join("."),
763
+ properties: record.properties,
764
+ });
765
+ };
766
+ }
767
+
768
+ // Pre-compile configuration for non-default cases
769
+ const isTemplateMessage = options.message === "template";
770
+ const propertiesOption = options.properties ?? "nest:properties";
771
+
772
+ // Pre-compile category joining strategy
577
773
  let joinCategory: (category: readonly string[]) => string | readonly string[];
578
774
  if (typeof options.categorySeparator === "function") {
579
775
  joinCategory = options.categorySeparator;
@@ -583,34 +779,11 @@ export function getJsonLinesFormatter(
583
779
  category.join(separator);
584
780
  }
585
781
 
586
- let getMessage: TextFormatter;
587
- if (options.message === "template") {
588
- getMessage = (record: LogRecord): string => {
589
- if (typeof record.rawMessage === "string") {
590
- return record.rawMessage;
591
- }
592
- let msg = "";
593
- for (let i = 0; i < record.rawMessage.length; i++) {
594
- msg += i % 2 < 1 ? record.rawMessage[i] : "{}";
595
- }
596
- return msg;
597
- };
598
- } else {
599
- getMessage = (record: LogRecord): string => {
600
- let msg = "";
601
- for (let i = 0; i < record.message.length; i++) {
602
- msg += i % 2 < 1
603
- ? record.message[i]
604
- : JSON.stringify(record.message[i]);
605
- }
606
- return msg;
607
- };
608
- }
609
-
610
- const propertiesOption = options.properties ?? "nest:properties";
782
+ // Pre-compile properties handling strategy
611
783
  let getProperties: (
612
784
  properties: Record<string, unknown>,
613
785
  ) => Record<string, unknown>;
786
+
614
787
  if (propertiesOption === "flatten") {
615
788
  getProperties = (properties) => properties;
616
789
  } else if (propertiesOption.startsWith("prepend:")) {
@@ -640,6 +813,38 @@ export function getJsonLinesFormatter(
640
813
  );
641
814
  }
642
815
 
816
+ // Pre-compile message rendering function
817
+ let getMessage: (record: LogRecord) => string;
818
+
819
+ if (isTemplateMessage) {
820
+ getMessage = (record: LogRecord): string => {
821
+ if (typeof record.rawMessage === "string") {
822
+ return record.rawMessage;
823
+ }
824
+ let msg = "";
825
+ for (let i = 0; i < record.rawMessage.length; i++) {
826
+ msg += i % 2 < 1 ? record.rawMessage[i] : "{}";
827
+ }
828
+ return msg;
829
+ };
830
+ } else {
831
+ getMessage = (record: LogRecord): string => {
832
+ const msgLen = record.message.length;
833
+
834
+ if (msgLen === 1) {
835
+ return record.message[0] as string;
836
+ }
837
+
838
+ let msg = "";
839
+ for (let i = 0; i < msgLen; i++) {
840
+ msg += (i % 2 < 1)
841
+ ? record.message[i]
842
+ : JSON.stringify(record.message[i]);
843
+ }
844
+ return msg;
845
+ };
846
+ }
847
+
643
848
  return (record: LogRecord): string => {
644
849
  return JSON.stringify({
645
850
  "@timestamp": new Date(record.timestamp).toISOString(),
package/logger.ts CHANGED
@@ -1343,46 +1343,77 @@ export function parseMessageTemplate(
1343
1343
  template: string,
1344
1344
  properties: Record<string, unknown>,
1345
1345
  ): readonly unknown[] {
1346
+ const length = template.length;
1347
+ if (length === 0) return [""];
1348
+
1349
+ // Fast path: no placeholders
1350
+ if (!template.includes("{")) return [template];
1351
+
1346
1352
  const message: unknown[] = [];
1347
- let part = "";
1348
- for (let i = 0; i < template.length; i++) {
1349
- const char = template.charAt(i);
1350
- const nextChar = template.charAt(i + 1);
1351
-
1352
- if (char === "{" && nextChar === "{") {
1353
- // Escaped { character
1354
- part = part + char;
1355
- i++;
1356
- } else if (char === "}" && nextChar === "}") {
1357
- // Escaped } character
1358
- part = part + char;
1359
- i++;
1360
- } else if (char === "{") {
1361
- // Start of a placeholder
1362
- message.push(part);
1363
- part = "";
1364
- } else if (char === "}") {
1365
- // End of a placeholder
1353
+ let startIndex = 0;
1354
+
1355
+ for (let i = 0; i < length; i++) {
1356
+ const char = template[i];
1357
+
1358
+ if (char === "{") {
1359
+ const nextChar = i + 1 < length ? template[i + 1] : "";
1360
+
1361
+ if (nextChar === "{") {
1362
+ // Escaped { character - skip and continue
1363
+ i++; // Skip the next {
1364
+ continue;
1365
+ }
1366
+
1367
+ // Find the closing }
1368
+ const closeIndex = template.indexOf("}", i + 1);
1369
+ if (closeIndex === -1) {
1370
+ // No closing } found, treat as literal text
1371
+ continue;
1372
+ }
1373
+
1374
+ // Add text before placeholder
1375
+ const beforeText = template.slice(startIndex, i);
1376
+ message.push(beforeText.replace(/{{/g, "{").replace(/}}/g, "}"));
1377
+
1378
+ // Extract and process placeholder key
1379
+ const key = template.slice(i + 1, closeIndex);
1380
+
1381
+ // Resolve property value
1366
1382
  let prop: unknown;
1367
- if (part.match(/^\s*\*\s*$/)) {
1368
- prop = part in properties
1369
- ? properties[part]
1383
+
1384
+ // Check for wildcard patterns
1385
+ const trimmedKey = key.trim();
1386
+ if (trimmedKey === "*") {
1387
+ // This is a wildcard pattern
1388
+ prop = key in properties
1389
+ ? properties[key]
1370
1390
  : "*" in properties
1371
1391
  ? properties["*"]
1372
1392
  : properties;
1373
- } else if (part.match(/^\s|\s$/)) {
1374
- prop = part in properties ? properties[part] : properties[part.trim()];
1375
1393
  } else {
1376
- prop = properties[part];
1394
+ // Regular property lookup with possible whitespace handling
1395
+ if (key !== trimmedKey) {
1396
+ // Key has leading/trailing whitespace
1397
+ prop = key in properties ? properties[key] : properties[trimmedKey];
1398
+ } else {
1399
+ // Key has no leading/trailing whitespace
1400
+ prop = properties[key];
1401
+ }
1377
1402
  }
1403
+
1378
1404
  message.push(prop);
1379
- part = "";
1380
- } else {
1381
- // Default case
1382
- part = part + char;
1405
+ i = closeIndex; // Move to the }
1406
+ startIndex = i + 1;
1407
+ } else if (char === "}" && i + 1 < length && template[i + 1] === "}") {
1408
+ // Escaped } character - skip
1409
+ i++; // Skip the next }
1383
1410
  }
1384
1411
  }
1385
- message.push(part);
1412
+
1413
+ // Add remaining text
1414
+ const remainingText = template.slice(startIndex);
1415
+ message.push(remainingText.replace(/{{/g, "{").replace(/}}/g, "}"));
1416
+
1386
1417
  return message;
1387
1418
  }
1388
1419
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/logtape",
3
- "version": "1.0.0-dev.246+c7630de7",
3
+ "version": "1.0.0-dev.247+ffe631a6",
4
4
  "description": "Simple logging library with zero dependencies for Deno/Node.js/Bun/browsers",
5
5
  "keywords": [
6
6
  "logging",