Repository: pinojs/pino
Pull request #1778 (2023-08-08 16:05:07)

feat(#1595): improve stream entry type
I improved the StreamEntry type with generics to enable custom levels.

Relate issue #1595 


Patch:
diff --git a/browser.js b/browser.js
index f9f8df0..8322842 100644
--- a/browser.js
+++ b/browser.js
@@ -44,7 +44,7 @@ function appendChildLogger (parentLogger, childLogger) {
 function setupBaseLogFunctions (logger, levels, proto) {
   const logFunctions = {}
   levels.forEach(level => {
-    logFunctions[level] = proto[level] ? proto[level] : (_console[level] || _console[logFallbackMap[level] ?? 'log'] || noop)
+    logFunctions[level] = proto[level] ? proto[level] : (_console[level] || _console[logFallbackMap[level] || 'log'] || noop)
   })
   logger[baseLogFunctionSymbol] = logFunctions
 }
diff --git a/lib/tools.js b/lib/tools.js
index 92bd23b..d67b2a4 100644
--- a/lib/tools.js
+++ b/lib/tools.js
@@ -156,7 +156,8 @@ function asJson (obj, msg, num, time) {
           value = (stringifier || stringify)(value, stringifySafe)
       }
       if (value === undefined) continue
-      propStr += ',"' + key + '":' + value
+      const strKey = asString(key)
+      propStr += ',' + strKey + ':' + value
     }
   }
 
diff --git a/pino.d.ts b/pino.d.ts
index b681ee8..f6ed8c8 100644
--- a/pino.d.ts
+++ b/pino.d.ts
@@ -284,18 +284,18 @@ declare namespace pino {
 
     type DestinationStreamWithMetadata = DestinationStream & ({ [symbols.needsMetadataGsym]?: false } | DestinationStreamHasMetadata);
 
-    interface StreamEntry {
+    interface StreamEntry<TLevel = Level> {
         stream: DestinationStream
-        level?: Level
+        level?: TLevel
     }
 
-    interface MultiStreamRes {
+    interface MultiStreamRes<TOriginLevel = Level> {
         write: (data: any) => void,
-        add: (dest: StreamEntry | DestinationStream) => MultiStreamRes,
+        add: <TLevel = Level>(dest: StreamEntry<TLevel> | DestinationStream) => MultiStreamRes<TOriginLevel & TLevel>,
         flushSync: () => void,
         minLevel: number,
-        streams: StreamEntry[],
-        clone(level: Level): MultiStreamRes,
+        streams: StreamEntry<TOriginLevel>[],
+        clone<TLevel = Level>(level: TLevel): MultiStreamRes<TLevel>,
     }
 
     interface LevelMapping {
@@ -775,10 +775,10 @@ declare namespace pino {
         options: TransportSingleOptions<TransportOptions> | TransportMultiOptions<TransportOptions> | TransportPipelineOptions<TransportOptions>
     ): ThreadStream
 
-    export function multistream(
-        streamsArray: (DestinationStream | StreamEntry)[] | DestinationStream | StreamEntry,
+    export function multistream<TLevel = Level>(
+        streamsArray: (DestinationStream | StreamEntry<TLevel>)[] | DestinationStream | StreamEntry<TLevel>,
         opts?: MultiStreamOptions
-    ): MultiStreamRes
+    ): MultiStreamRes<TLevel>
 }
 
 //// Callable default export
@@ -833,8 +833,8 @@ export interface LogEvent extends pino.LogEvent {}
 export interface LogFn extends pino.LogFn {}
 export interface LoggerOptions extends pino.LoggerOptions {}
 export interface MultiStreamOptions extends pino.MultiStreamOptions {}
-export interface MultiStreamRes extends pino.MultiStreamRes {}
-export interface StreamEntry extends pino.StreamEntry {}
+export interface MultiStreamRes<TLevel = Level> extends pino.MultiStreamRes<TLevel> {}
+export interface StreamEntry<TLevel = Level> extends pino.StreamEntry<TLevel> {}
 export interface TransportBaseOptions extends pino.TransportBaseOptions {}
 export interface TransportMultiOptions extends pino.TransportMultiOptions {}
 export interface TransportPipelineOptions extends pino.TransportPipelineOptions {}


Test patch:
diff --git a/test/basic.test.js b/test/basic.test.js
index c8a6962..a7ad5ba 100644
--- a/test/basic.test.js
+++ b/test/basic.test.js
@@ -420,6 +420,23 @@ test('correctly escape msg strings with unclosed double quote', async ({ same })
   })
 })
 
+test('correctly escape quote in a key', async ({ same }) => {
+  const stream = sink()
+  const instance = pino(stream)
+  const obj = { 'some"obj': 'world' }
+  instance.info(obj, 'a string')
+  const result = await once(stream, 'data')
+  delete result.time
+  same(result, {
+    level: 30,
+    pid,
+    hostname,
+    msg: 'a string',
+    'some"obj': 'world'
+  })
+  same(Object.keys(obj), ['some"obj'])
+})
+
 // https://github.com/pinojs/pino/issues/139
 test('object and format string', async ({ same }) => {
   const stream = sink()
diff --git a/test/types/pino-multistream.test-d.ts b/test/types/pino-multistream.test-d.ts
index 6006f9c..29f09d7 100644
--- a/test/types/pino-multistream.test-d.ts
+++ b/test/types/pino-multistream.test-d.ts
@@ -2,25 +2,27 @@ import { expectType } from 'tsd'
 
 import { createWriteStream } from 'fs'
 
-import pino from '../../pino'
-import { multistream } from "../../pino";
+import pino, { multistream } from '../../pino'
 
 const streams = [
   { stream: process.stdout },
   { stream: createWriteStream('') },
   { level: 'error' as const, stream: process.stderr },
-  { level: 'fatal' as const, stream: createWriteStream('') }
+  { level: 'fatal' as const, stream: process.stderr },
 ]
 
 expectType<pino.MultiStreamRes>(pino.multistream(process.stdout))
 expectType<pino.MultiStreamRes>(pino.multistream([createWriteStream('')]))
-expectType<pino.MultiStreamRes>(pino.multistream({ level: 'error' as const, stream: process.stderr }))
-expectType<pino.MultiStreamRes>(pino.multistream([{ level: 'fatal' as const, stream: createWriteStream('') }]))
+expectType<pino.MultiStreamRes<'error'>>(pino.multistream({ level: 'error' as const, stream: process.stderr }))
+expectType<pino.MultiStreamRes<'fatal'>>(pino.multistream([{ level: 'fatal' as const, stream: createWriteStream('') }]))
+
+expectType<pino.MultiStreamRes<'error' | 'fatal'>>(pino.multistream(streams))
+expectType<pino.MultiStreamRes<'error' | 'fatal'>>(pino.multistream(streams, {}))
+expectType<pino.MultiStreamRes<'error' | 'fatal'>>(pino.multistream(streams, { levels: { 'info': 30 } }))
+expectType<pino.MultiStreamRes<'error' | 'fatal'>>(pino.multistream(streams, { dedupe: true }))
+expectType<pino.MultiStreamRes<'error' | 'fatal'>>(pino.multistream(streams[0]).add(streams[1]))
+expectType<pino.MultiStreamRes<'error' | 'fatal'>>(multistream(streams))
+expectType<pino.MultiStreamRes<'error'>>(multistream(streams).clone('error'))
 
-expectType<pino.MultiStreamRes>(pino.multistream(streams))
-expectType<pino.MultiStreamRes>(pino.multistream(streams, {}))
-expectType<pino.MultiStreamRes>(pino.multistream(streams, { levels: { 'info': 30 } }))
-expectType<pino.MultiStreamRes>(pino.multistream(streams, { dedupe: true }))
-expectType<pino.MultiStreamRes>(pino.multistream(streams[0]).add(streams[1]))
 
 expectType<pino.MultiStreamRes>(multistream(process.stdout));
diff --git a/test/types/pino.ts b/test/types/pino.ts
index 5ac2d93..c1cf8c0 100644
--- a/test/types/pino.ts
+++ b/test/types/pino.ts
@@ -1,6 +1,7 @@
-import { pino } from '../../pino'
+import { StreamEntry, pino } from '../../pino'
 import { join } from 'path'
 import { tmpdir } from 'os'
+import pinoPretty from 'pino-pretty'
 
 const destination = join(
     tmpdir(),
@@ -40,3 +41,38 @@ const transports = pino.transport({targets: [
 ]})
 const loggerMulti = pino(transports)
 loggerMulti.info('test2')
+
+// custom levels
+
+const customLevels = {
+    debug   : 1,
+    info    : 2,
+    network : 3,
+    error   : 4,
+};
+
+type CustomLevels = keyof typeof customLevels;
+
+const pinoOpts = {
+    level: 'debug',
+    useOnlyCustomLevels: true,
+    customLevels: customLevels,
+};
+
+const multistreamOpts = {
+    dedupe: true,
+    levels: customLevels
+};
+
+const streams: StreamEntry<CustomLevels>[] = [
+    { level : 'debug',   stream : pinoPretty() },
+    { level : 'info',    stream : pinoPretty() },
+    { level : 'network', stream : pinoPretty() },
+    { level : 'error',   stream : pinoPretty() },
+];
+
+const loggerCustomLevel = pino(pinoOpts, pino.multistream(streams, multistreamOpts));
+loggerCustomLevel.debug('test3')
+loggerCustomLevel.info('test4')
+loggerCustomLevel.error('test5')
+loggerCustomLevel.network('test6')