Repository: pinojs/pino
Pull request #492 (2018-08-18 12:47:53)

Add useLevelLabels option
I am sure we have had more issues than just issue #279 regarding this, but I cannot find them easily. And I know I have argued our stance against this in the past. However, I have had to evolve my opinion:

1. The labels are just as parseable
1. We claim to follow [Bunyan](https://www.npmjs.com/package/bunyan): Bunyan outputs labels
1. We claim to be a Log4j style logger: Log4j defaults to outputting labels -- https://www.mkyong.com/logging/log4j-hello-world-example/

But the ultimate reason is selfish: I want to use Pino in my work at Knock.com and using transports is not feasible. Our platform is built upon AWS. Thus:

1. AWS Lambda doesn't support transports
1. AWS ECS could potentially support transports, but it would unnecessarily complicate deployments

Additionally, we are a polyglot environment. I need to be able to conform whatever logger I use to our internal log format specification. Using labels instead of integers makes this easier across multiple languages.

Initially I attempted to solve this locally through the global serializer, but that serializer doesn't receive the final log object. Maybe I could do this through the meta API, but both of these would just be hacks. I think we should be more flexible by:

1. Asserting a strong opinion about the usage of Pino via documentation and instance defaults
1. Recognizing that others have use cases outside of what we recommend and allowing them to make the choices they need to make

This feature, along with the `endSym` feature in Pino@5, allows me to accomplish that in my environment.

mcollina: Can you clarify to role of `endSym` here?
jsumners: `endSym` doesn't have anything to do with this PR. I am merely stating that it is another feature that, in addition to this proposed one, helps me accomplish what I need to accomplish.

Patch:
diff --git a/docs/api.md b/docs/api.md
index 6570fe4..9df3f6b 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1,4 +1,4 @@
-# API 
+# API
 
 * [pino() => logger](#export)
   * [options](#options)
@@ -50,8 +50,8 @@ The name of the logger. When set adds a `name` field to every JSON line logged.
 Default: `'info'`
 
 One of `'fatal'`, `'error'`, `'warn'`, `'info`', `'debug'`, `'trace'` or `silent`.
- 
-Additional levels can be added to the instance via the `customLevels` option. 
+
+Additional levels can be added to the instance via the `customLevels` option.
 
 * See [`customLevels` option](#opt-customlevels)
 
@@ -61,8 +61,8 @@ Additional levels can be added to the instance via the `customLevels` option.
 Default: `undefined`
 
 Use this option to define additional logging levels.
-The keys of the object correspond the namespace of the log level, 
-and the values should be the numerical value of the level. 
+The keys of the object correspond the namespace of the log level,
+and the values should be the numerical value of the level.
 
 ```js
 const logger = pino({
@@ -73,18 +73,18 @@ const logger = pino({
 logger.foo('hi')
 ```
 
-#### `redact` (Array | Object): 
+#### `redact` (Array | Object):
 
 Default: `undefined`
 
-As an array, the `redact` option specifies paths that should 
-have their values redacted from any log output. 
+As an array, the `redact` option specifies paths that should
+have their values redacted from any log output.
 
 Each path must be a string using a syntax which corresponds to JavaScript dot and bracket notation.
 
-If an object is supplied, three options can be specified: 
+If an object is supplied, three options can be specified:
   * `paths` (array): Required. An array of paths. See [redaction - Path Syntax ⇗](/docs/redaction.md#paths) for specifics.
-  * `censor` (String): Optional. A value to overwrite key which are to be redacted. Default: `'[Redacted]'` 
+  * `censor` (String): Optional. A value to overwrite key which are to be redacted. Default: `'[Redacted]'`
   * `remove` (Boolean): Optional. Instead of censoring the value, remove both the key and the value. Default: `false`
 
 **WARNING**: Never allow user input to define redacted paths.
@@ -99,7 +99,7 @@ Default: `{err: pino.stdSerializers.err}`
 
 An object containing functions for custom serialization of objects.
 These functions should return an JSONifiable object and they
-should never throw. When logging an object, each top-level property 
+should never throw. When logging an object, each top-level property
 matching the exact key of a serializer will be serialized using the defined serializer.
 
 * See [pino.stdSerializers](#pino-stdserializers)
@@ -108,18 +108,18 @@ matching the exact key of a serializer will be serialized using the defined seri
 
 Default: `undefined`
 
-The `serializers` object may contain a key which is the global symbol: `Symbol.for('pino.*')`. 
+The `serializers` object may contain a key which is the global symbol: `Symbol.for('pino.*')`.
 This will act upon the complete log object rather than corresponding to a particular key.
 
 #### `base` (Object)
 
 Default: `{pid: process.pid, hostname: os.hostname}`
 
-Key-value object added as child logger to each log line. 
+Key-value object added as child logger to each log line.
 
 Set to `null` to avoid adding `pid` and `hostname` properties to each log.
 
-#### `enabled` (Boolean) 
+#### `enabled` (Boolean)
 
 Default: `true`
 
@@ -142,16 +142,16 @@ representation of the time, e.g. `,"time":1493426328206` (which is the default).
 
 If set to `false`, no timestamp will be included in the output.
 See [stdTimeFunctions](#pino-stdtimefunctions) for a set of available functions
-for passing in as a value for this option. 
+for passing in as a value for this option.
 
 **Caution**: attempting to format time in-process will significantly impact logging performance.
 
-<a id=opt-messagekey></a>  
+<a id=opt-messagekey></a>
 #### `messageKey` (String)
 
 Default: `'msg'`
 
-The string key for the 'message' in the JSON object. 
+The string key for the 'message' in the JSON object.
 
 <a id=prettyPrint></a>
 #### `prettyPrint` (Boolean | Object)
@@ -171,9 +171,17 @@ must be installed as a separate dependency:
 npm install pino-pretty
 ```
 
+<a id="useLevelLabels"></a>
+#### `useLevelLabels` (Boolean)
+
+Default: `false`
+
+Enables printing of level labels instead of level values in the printed logs.
+Warning: this option may not be supported by downstream transports.
+
 #### `browser` (Object)
 
-Browser only, may have `asObject` and `write` keys. This option is separately 
+Browser only, may have `asObject` and `write` keys. This option is separately
 documented in the [Browser API ⇗](/docs/browser.md) documentation.
 
 * See [Browser API ⇗](/docs/browser.md)
@@ -185,7 +193,7 @@ Default: `pino.destination(1)` (STDOUT)
 
 The `destination` parameter, at a minimum must be an object with a `write` method.
 An ordinary Node.js `stream` can be passed as the destination (such as the result
-of `fs.createWriteStream`) but for peak log writing performance it is strongly 
+of `fs.createWriteStream`) but for peak log writing performance it is strongly
 recommended to use `pino.destination` or `pino.extreme` to create the destination stream.
 
 ```js
@@ -195,7 +203,7 @@ const stdoutLogger = require('pino')()
 // destination param may be in first position when no options:
 const fileLogger = require('pino')( pino.destination('/log/path'))
 
-// use the stderr file handle to log to stderr: 
+// use the stderr file handle to log to stderr:
 const opts = {name: 'my-logger'}
 const stderrLogger = require('pino')(opts, pino.destination(2))
 
@@ -205,15 +213,15 @@ const fileLogger = require('pino')('/log/path')
 
 On AWS Lambda the default destination is `process.stdout` instead.
 
-* See [`pino.destination`](#pino-destination) 
-* See [`pino.extreme`](#pino-extreme) 
+* See [`pino.destination`](#pino-destination)
+* See [`pino.extreme`](#pino-extreme)
 
 #### `destination[Symbol.for('pino.metadata')]`
 
 Default: `false`
 
-Using the global symbol `Symbol.for('pino.metadata')` as a key on the `destination` parameter and 
-setting the key it to `true`, indicates that the following properties should be 
+Using the global symbol `Symbol.for('pino.metadata')` as a key on the `destination` parameter and
+setting the key it to `true`, indicates that the following properties should be
 set on the `destination` object after each log line is written:
 
 * the last logging level as `destination.lastLevel`
@@ -225,9 +233,9 @@ set on the `destination` object after each log line is written:
   loggers)
 
 For a full reference for using `Symbol.for('pino.metadata')`, see the [`pino-multi-stream` ⇗](https://github.com/pinojs/pino-multi-stream)
-module. 
+module.
 
-The following is a succinct usage example: 
+The following is a succinct usage example:
 
 ```js
 const dest = pino.destination('/dev/null')
@@ -236,7 +244,7 @@ const logger = pino(dest)
 logger.info({a: 1}, 'hi')
 const { lastMsg, lastLevel, lastObj, lastTime} = dest
 console.log(
-  'Logged message "%s" at level %d with object %o at time %s', 
+  'Logged message "%s" at level %d with object %o at time %s',
   lastMsg, lastLevel, lastObj, lastTime
 ) // Logged message "hi" at level 30 with object { a: 1 } at time 1531590545089
 ```
@@ -251,9 +259,9 @@ The logger instance is the object returned by the main exported
 
 The primary purpose of the logger instance is to provide logging methods.
 
-The default logging methods are `trace`, `debug`, `info`, `warn`, and `fatal`. 
+The default logging methods are `trace`, `debug`, `info`, `warn`, and `fatal`.
 
-Each logging method has the following signature: 
+Each logging method has the following signature:
 `([mergingObject], [message], [...interpolationValues])`.
 
 The parameters are explained below using the `logger.info` method but the same applies to all logging methods.
@@ -263,7 +271,7 @@ The parameters are explained below using the `logger.info` method but the same a
 <a id=mergingobject></a>
 #### `mergingObject` (Object)
 
-An object can optionally be supplied as the first parameter. Each enumerable key and value 
+An object can optionally be supplied as the first parameter. Each enumerable key and value
 of the `mergingObject` is copied in to the JSON log line.
 
 ```js
@@ -274,10 +282,10 @@ logger.info({MIX: {IN: true}})
 <a id=message></a>
 #### `message` (String)
 
-A `message` string can optionally be supplied as the first parameter, or 
+A `message` string can optionally be supplied as the first parameter, or
 as the second parameter after supplying a `mergingObject`.
 
-By default, the contents of the `message` parameter will be merged into the 
+By default, the contents of the `message` parameter will be merged into the
 JSON log line under the `msg` key:
 
 ```js
@@ -286,22 +294,22 @@ logger.info('hello world')
 ```
 
 The `message` parameter takes precedence over the `mergedObject`.
-That is, if a `mergedObject` contains a `msg` property, and a `message` parameter 
-is supplied in addition, the `msg` property in the output log will be the value of 
+That is, if a `mergedObject` contains a `msg` property, and a `message` parameter
+is supplied in addition, the `msg` property in the output log will be the value of
 the `message` parameter not the value of the `msg` property on the `mergedObject`.
 
 The `messageKey` option can be used at instantiation time to change the namespace
 from `msg` to another string as preferred.
- 
-The `message` string may contain a printf style string with support for 
-the following placeholders: 
 
-* `%s` – string placeholder 
+The `message` string may contain a printf style string with support for
+the following placeholders:
+
+* `%s` – string placeholder
 * `%d` – digit placeholder)
 * `%O`, `%o` and `%j` – object placeholder
 
 Values supplied as additional arguments to the logger method will
-then be interpolated accordingly. 
+then be interpolated accordingly.
 
 * See [`messageKey` pino option](#opt-messagekey)
 * See [`...interpolationValues` log method parameter](#interpolationvalues)
@@ -309,7 +317,7 @@ then be interpolated accordingly.
 <a id=interpolationvalues></a>
 #### `...interpolationValues` (Any)
 
-All arguments supplied after `message` are serialized and interpolated according 
+All arguments supplied after `message` are serialized and interpolated according
 to any supplied printf-style placeholders (`%s`, `%d`, `%o`|`%O`|`%j`)
 or else concatenated together with the `message` string to form the final
 output `msg` value for the JSON log line.
@@ -356,7 +364,7 @@ Write an `'info'` level log, if the configured `level` allows for it.
 
 * See [`mergingObject` log method parameter](#mergingobject)
 * See [`message` log method parameter](#message)
-* See [`...interpolationValues` log method parameter](#interpolationvalues) 
+* See [`...interpolationValues` log method parameter](#interpolationvalues)
 
 <a id="warn"></a>
 ### `logger.warn([mergingObject], [message], [...interpolationValues])`
@@ -389,21 +397,21 @@ Write a `'fatal'` level log, if the configured `level` allows for it.
 <a id="child"></a>
 ### `logger.child(bindings) => logger`
 
-The `logger.child` method allows for the creation of stateful loggers, 
+The `logger.child` method allows for the creation of stateful loggers,
 where key-value pairs can be pinned to a logger causing them to be output
 on every log line.
 
 Child loggers use the same output stream as the parent and inherit
 the current log level of the parent at the time they are spawned.
 
-The log level of a child is mutable. It can be set independently 
-of the parent either by setting the [`level`](#level) accessor after creating 
+The log level of a child is mutable. It can be set independently
+of the parent either by setting the [`level`](#level) accessor after creating
 the child logger or using the reserved [`bindings.level`](#bindingslevel-string) key.
 
 #### `bindings` (Object)
 
-An object of key-value pairs to include in every log line output 
-via the returned child logger. 
+An object of key-value pairs to include in every log line output
+via the returned child logger.
 
 ```js
 const child = logger.child({ MIX: {IN: 'always'} })
@@ -417,7 +425,7 @@ The `bindings` object may contain any key except for reserved configuration keys
 
 ##### `bindings.level` (String)
 
-If a `level` property is present in the `bindings` object passed to `logger.child` 
+If a `level` property is present in the `bindings` object passed to `logger.child`
 it will override the child logger level.
 
 ```js
@@ -431,8 +439,8 @@ child.debug('debug!') // will log as the `level` property set the level to debug
 
 Child loggers inherit the [serializers](#opt-serializers) from the parent logger.
 
-Setting the `serializers` key of the `bindings` object will override 
-any configured parent serializers. 
+Setting the `serializers` key of the `bindings` object will override
+any configured parent serializers.
 
 ```js
 const logger = require('pino')()
@@ -453,11 +461,11 @@ Flushes the content of the buffer when using a `pino.extreme` destination.
 
 This is an asynchronous, fire and forget, operation.
 
-The use case is primarily for Extreme mode logging, which may hold up to 
-4KiB of logs. The `logger.flush` method can be used to flush the logs 
-on an long interval, say ten seconds. Such a strategy can provide an 
-optimium balanace between extremely efficient logging at high demand periods 
-and safer logging at low demand periods. 
+The use case is primarily for Extreme mode logging, which may hold up to
+4KiB of logs. The `logger.flush` method can be used to flush the logs
+on an long interval, say ten seconds. Such a strategy can provide an
+optimium balanace between extremely efficient logging at high demand periods
+and safer logging at low demand periods.
 
 * See [`pino.extreme`](#pino-extreme)
 * See [`destination` parameter](#destination)
@@ -466,9 +474,9 @@ and safer logging at low demand periods.
 <a id="level"></a>
 ### `logger.level` (String) [Getter/Setter]
 
-Set this property to the desired logging level. 
+Set this property to the desired logging level.
 
-The core levels and their values are as follows: 
+The core levels and their values are as follows:
 
 |                                                                     |
 |:-------------------------------------------------------------------:|
@@ -503,10 +511,10 @@ Defines the method name of the new level.
 
 #### `levelValue` (Number)
 
-Defines the associated minimum threshold value for the level, and 
+Defines the associated minimum threshold value for the level, and
 therefore where it sits in order of priority among other levels.
 
-* See [`logger.level`](#level) 
+* See [`logger.level`](#level)
 
 <a id="levelVal"></a>
 ### `logger.levelVal` (Number)
@@ -525,10 +533,10 @@ if (logger.levelVal === 30) {
 Levels are mapped to values to determine the minimum threshold that a
 logging method should be enabled at (see [`logger.level`](#level)).
 
-The `logger.levels` property holds the mappings between levels and values, 
+The `logger.levels` property holds the mappings between levels and values,
 and vice versa.
 
-```sh 
+```sh
 $ node -p "require('pino')().levels"
 ```
 
@@ -557,9 +565,9 @@ register it's own serializer upon instantiation the serializers of the parent wi
 
 The logger instance is also an [`EventEmitter ⇗`](https://nodejs.org/dist/latest/docs/api/events.html#events_class_eventemitter)
 
-A listener function can be attached to a logger via the `level-change` event 
+A listener function can be attached to a logger via the `level-change` event
 
-The listener is passed four arguments: 
+The listener is passed four arguments:
 
 * `levelLabel` – the new level string, e.g `trace`
 * `levelValue` – the new level number, e.g `10`
@@ -594,7 +602,7 @@ Also available on the exported `pino` function.
 <a id="pino-destination"></a>
 ### `pino.destination([target]) => SonicBoom`
 
-Create a Pino Destination instance: a stream-like object with 
+Create a Pino Destination instance: a stream-like object with
 significantly more throughput (over 30%) than a standard Node.js stream.
 
 ```js
@@ -603,12 +611,12 @@ const logger = pino(pino.destination('./my-file'))
 const logger2 = pino(pino.destination())
 ```
 
-The `pino.destination` method may be passed a file path or a numerical file descriptor. 
+The `pino.destination` method may be passed a file path or a numerical file descriptor.
 By default, `pino.destination` will use `process.stdout.fd` (1) as the file descriptor.
 
 `pino.destination` is implemented on [`sonic-boom` ⇗]](https://github.com/mcollina/sonic-boom).
 
-A `pino.destination` instance can also be used to reopen closed files 
+A `pino.destination` instance can also be used to reopen closed files
 (for example, for some log rotation scenarios), see [Reopening log files](/docs/help.md#reopening).
 
 On AWS Lambda we recommend to call `destination.flushSync()` at the end
@@ -630,13 +638,13 @@ const logger = pino(pino.extreme('./my-file'))
 const logger2 = pino(pino.extreme())
 ```
 
-The `pino.extreme` method may be passed a file path or a numerical file descriptor. 
+The `pino.extreme` method may be passed a file path or a numerical file descriptor.
 By default, `pino.destination` will use `process.stdout.fd` (1) as the file descriptor.
 
 `pino.extreme` is implemented with the [`sonic-boom` ⇗](https://github.com/mcollina/sonic-boom)
 module.
 
-A `pino.extreme` instance can also be used to reopen closed files 
+A `pino.extreme` instance can also be used to reopen closed files
 (for example, for some log rotation scenarios), see [Reopening log files](/docs/help.md#reopening).
 
 On AWS Lambda we recommend to call `extreme.flushSync()` at the end
@@ -650,18 +658,18 @@ of each function execution to avoid losing data.
 <a id="pino-final"></a>
 ### `pino.final(logger, handler) => Function`
 
-The `pino.final` method supplies an exit listener function that can be 
-supplied to process exit events such as `exit`, `uncaughtException`, 
-`SIGHUP` and so on. 
+The `pino.final` method supplies an exit listener function that can be
+supplied to process exit events such as `exit`, `uncaughtException`,
+`SIGHUP` and so on.
 
-The exit listener function will call the supplied `handler` function 
+The exit listener function will call the supplied `handler` function
 with an error object (or else `null`), a `finalLogger` instance followed
-by any additional arguments the `handler` may be called with. 
-The `finalLogger` is a specialist logger that synchronously flushes 
-on every write. This is important to gaurantee final log writes, 
-both when using `pino.destination` or `pino.extreme` targets. 
+by any additional arguments the `handler` may be called with.
+The `finalLogger` is a specialist logger that synchronously flushes
+on every write. This is important to gaurantee final log writes,
+both when using `pino.destination` or `pino.extreme` targets.
 
-Since final log writes cannot be gauranteed with normal Node.js streams, 
+Since final log writes cannot be gauranteed with normal Node.js streams,
 if the `destination` parameter of the `logger` supplied to `pino.final`
 is a Node.js stream `pino.final` will throw. It's highly recommended
 for both performance and safety to use `pino.destination`/`pino.extreme`
@@ -689,11 +697,11 @@ The `pino.stdSerializers` object provides functions for serializing objects comm
 <a id="pino-stdtimefunctions"></a>
 ### `pino.stdTimeFunctions` (Object)
 
-The [`timestamp`](#opt-timestamp) option can accept a function which determines the 
+The [`timestamp`](#opt-timestamp) option can accept a function which determines the
 `timestamp` value in a log line.
 
-The `pino.stdTimeFunctions` object provides a very small set of common functions for generating the 
-`timestamp` property. These consist of the following 
+The `pino.stdTimeFunctions` object provides a very small set of common functions for generating the
+`timestamp` property. These consist of the following
 
 * `pino.stdTimeFunctions.epochTime`: Milliseconds since Unix epoch (Default)
 * `pino.stdTimeFunctions.unixTime`: Seconds since Unix epoch
@@ -710,7 +718,7 @@ exposes the symbols used to hold non-public state and methods on the logger inst
 Access to the symbols allows logger state to be adjusted, and methods to be overriden or
 proxied for performant integration where necessary.
 
-The `pino.symbols` object is intended for library implementers and shouldn't be utilized 
+The `pino.symbols` object is intended for library implementers and shouldn't be utilized
 for general use.
 
 <a id="pino-version"></a>
diff --git a/docs/help.md b/docs/help.md
index 6a8cc09..2ba3113 100644
--- a/docs/help.md
+++ b/docs/help.md
@@ -13,12 +13,12 @@
 <a id="exit-logging"></a>
 ## Exit logging
 
-When a Node process crashes from uncaught exception, exits due to a signal, 
+When a Node process crashes from uncaught exception, exits due to a signal,
 or exits of it's own accord we may want to write some final logs – particularly
-in cases of error. 
+in cases of error.
 
 Writing to a Node.js stream on exit is not necessarily guaranteed, and naively writing
-to an Extreme Mode logger on exit will definitely lead to lost logs. 
+to an Extreme Mode logger on exit will definitely lead to lost logs.
 
 To write logs in an exit handler, create the handler with [`pino.final`](/docs/api.md#pino-final):
 
@@ -34,7 +34,7 @@ process.on('unhandledRejection', pino.final(logger, (err, finalLogger) => {
 }))
 ```
 
-The `finalLogger` is a special logger instance that will synchronously and reliably 
+The `finalLogger` is a special logger instance that will synchronously and reliably
 flush every log line. This is important in exit handlers, since no more asynchronous
 activity may be scheduled.
 
@@ -64,7 +64,7 @@ We would rotate our log files with logrotate, by adding the following to `/etc/l
 }
 ```
 
-The `copytruncate` configuration has a very slight possibility of lost log lines due 
+The `copytruncate` configuration has a very slight possibility of lost log lines due
 to a gap between copying and truncating - the truncate may occur after additional lines
 have been written. To perform log rotation without `copytruncate`, see the [Reopening log files](#reopening)
 help.
@@ -72,12 +72,12 @@ help.
 <a id="reopening"></a>
 ## Reopening log files
 
-In cases where a log rotation tool doesn't offer a copy-truncate capabilities, 
+In cases where a log rotation tool doesn't offer a copy-truncate capabilities,
 or where using them is deemed inappropriate `pino.destination` and `pino.extreme`
 destinations are able to reopen file paths after a file has been moved away.
 
-One way to use this is to set up a `SIGUSR2` or `SIGHUP` signal handler that 
-reopens the log file destination, making sure to write the process PID out 
+One way to use this is to set up a `SIGUSR2` or `SIGHUP` signal handler that
+reopens the log file destination, making sure to write the process PID out
 somewhere so the log rotation tool knows where to send the signal.
 
 ```js
@@ -90,7 +90,7 @@ const logger = require('pino')(dest)
 process.on('SIGHUP', () => dest.reopen())
 ```
 
-The log rotation tool can then be configured to send this signal to the process 
+The log rotation tool can then be configured to send this signal to the process
 after a log rotation event has occurred.
 
 Given a similar scenario as in the [Log rotation](#rotate) section a basic
@@ -109,7 +109,7 @@ Given a similar scenario as in the [Log rotation](#rotate) section a basic
            kill -HUP `cat /var/run/myapp.pid`
        endscript
 }
-``` 
+```
 
 <a id="multiple"></a>
 ## Saving to multiple files
@@ -131,7 +131,7 @@ node app.js | pino-tee warn ./warn-logs > ./all-logs
 
 <a id="filter-logs"></a>
 ## Log Filtering
-The Pino philosophy advocates common, pre-existing, system utilities. 
+The Pino philosophy advocates common, pre-existing, system utilities.
 
 Some recommendations in line with this philosophy are:
 
@@ -159,16 +159,17 @@ ExecStart=/bin/sh -c '/path/to/node app.js | pino-transport'
 ## How Pino handles duplicate keys
 
 Duplicate keys are possibly when a child logger logs an object with a key that
-collides with a key in the child loggers bindings. 
+collides with a key in the child loggers bindings.
 
 See the [child logger duplicate keys caveat](/docs/child-loggers.md#duplicate-keys-caveat)
 for information on this is handled.
 
 <a id="level-string"></a>
 ## Log levels as labels instead of numbers
-Pino log lines are meant to be parseable. Thus, there isn't any built-in option
-to change the level from the integer value to the string name. However, there
-are a couple of options:
+Pino log lines are meant to be parseable. Thus, Pino's default mode of opertaion
+is to print the level value instead of the string name. However, while it is
+possible to set the `useLevelLabels` option, we recommend using one of these
+options instead if you are able:
 
 1. If the only change desired is the name then a transport can be used. One such
 transport is [`pino-text-level-transport`](https://npm.im/pino-text-level-transport).
@@ -178,12 +179,12 @@ the logs human friendly.
 <a id="debug"></a>
 ## Pino with `debug`
 
-The popular [`debug`](http://npm.im/debug) is used in many modules across the ecosystem. 
+The popular [`debug`](http://npm.im/debug) is used in many modules across the ecosystem.
 
 The [`pino-debug`](http://github.com/pinojs/pino-debug) module
 can capture calls to `debug` loggers and run them
 through `pino` instead. This results in a 10x (20x in extreme mode)
-performance improvement - event though `pino-debug` is logging additional 
+performance improvement - event though `pino-debug` is logging additional
 data and wrapping it in JSON.
 
 To quickly enable this install [`pino-debug`](http://github.com/pinojs/pino-debug)
@@ -197,4 +198,4 @@ $ DEBUG=* node -r pino-debug app.js
 
 [`pino-debug`](http://github.com/pinojs/pino-debug) also offers fine grain control to map specific `debug`
 namespaces to `pino` log levels. See [`pino-debug`](http://github.com/pinojs/pino-debug)
-for more.
\ No newline at end of file
+for more.
diff --git a/lib/levels.js b/lib/levels.js
index a35e4b3..f98e01e 100644
--- a/lib/levels.js
+++ b/lib/levels.js
@@ -1,6 +1,6 @@
 'use strict'
 const flatstr = require('flatstr')
-const { lsCacheSym, levelValSym } = require('./symbols')
+const { lsCacheSym, levelValSym, useLevelLabelsSym } = require('./symbols')
 const { noop, genLog } = require('./tools')
 
 const levels = {
@@ -33,7 +33,9 @@ const initialLsCache = Object.keys(nums).reduce((o, k) => {
 
 function genLsCache (instance) {
   instance[lsCacheSym] = Object.keys(instance.levels.labels).reduce((o, k) => {
-    if (!(k in o)) o[k] = flatstr('{"level":' + Number(k))
+    o[k] = instance[useLevelLabelsSym]
+      ? `{"level":"${instance.levels.labels[k]}"`
+      : flatstr('{"level":' + Number(k))
     return o
   }, instance[lsCacheSym])
   return instance
diff --git a/lib/symbols.js b/lib/symbols.js
index e56adaf..4a1c883 100644
--- a/lib/symbols.js
+++ b/lib/symbols.js
@@ -3,6 +3,7 @@
 const setLevelSym = Symbol('pino.setLevel')
 const getLevelSym = Symbol('pino.getLevel')
 const levelValSym = Symbol('pino.levelVal')
+const useLevelLabelsSym = Symbol('pino.useLevelLabels')
 
 const lsCacheSym = Symbol('pino.lsCache')
 const chindingsSym = Symbol('pino.chindings')
@@ -30,6 +31,7 @@ module.exports = {
   setLevelSym,
   getLevelSym,
   levelValSym,
+  useLevelLabelsSym,
   lsCacheSym,
   chindingsSym,
   parsedChindingsSym,
diff --git a/pino.js b/pino.js
index a6f861d..ead709a 100644
--- a/pino.js
+++ b/pino.js
@@ -25,7 +25,8 @@ const {
   setLevelSym,
   endSym,
   formatOptsSym,
-  messageKeyStringSym
+  messageKeyStringSym,
+  useLevelLabelsSym
 } = symbols
 const { epochTime, nullTime } = time
 const { pid } = process
@@ -33,6 +34,7 @@ const hostname = os.hostname()
 const defaultErrorSerializer = serializers.err
 const defaultOptions = {
   level: 'info',
+  useLevelLabels: false,
   messageKey: 'msg',
   enabled: true,
   prettyPrint: false,
@@ -57,7 +59,8 @@ function pino (...args) {
     base,
     name,
     level,
-    customLevels
+    customLevels,
+    useLevelLabels
   } = opts
 
   assertNoLevelCollisions(pino.levels, customLevels)
@@ -83,6 +86,7 @@ function pino (...args) {
 
   const instance = {
     levels,
+    [useLevelLabelsSym]: useLevelLabels,
     [streamSym]: stream,
     [timeSym]: time,
     [stringifySym]: stringify,
@@ -95,7 +99,7 @@ function pino (...args) {
   }
   Object.setPrototypeOf(instance, proto)
 
-  if (customLevels) genLsCache(instance)
+  if (customLevels || useLevelLabels) genLsCache(instance)
 
   instance[setLevelSym](level)
 


Test patch:
diff --git a/test/levels.test.js b/test/levels.test.js
index 3f5b069..b30d701 100644
--- a/test/levels.test.js
+++ b/test/levels.test.js
@@ -227,3 +227,68 @@ test('levelVal is read only', async ({throws}) => {
   const instance = pino()
   throws(() => { instance.levelVal = 20 })
 })
+
+test('produces labels when told to', async ({is}) => {
+  const expected = [{
+    level: 'info',
+    msg: 'hello world'
+  }]
+  const instance = pino({useLevelLabels: true}, sink((result, enc, cb) => {
+    const current = expected.shift()
+    check(is, result, current.level, current.msg)
+    cb()
+  }))
+
+  instance.info('hello world')
+})
+
+test('children produce labels when told to', async ({is}) => {
+  const expected = [
+    {
+      level: 'info',
+      msg: 'child 1'
+    },
+    {
+      level: 'info',
+      msg: 'child 2'
+    }
+  ]
+  const instance = pino({useLevelLabels: true}, sink((result, enc, cb) => {
+    const current = expected.shift()
+    check(is, result, current.level, current.msg)
+    cb()
+  }))
+
+  const child1 = instance.child({name: 'child1'})
+  const child2 = child1.child({name: 'child2'})
+
+  child1.info('child 1')
+  child2.info('child 2')
+})
+
+test('produces labels for custom levels', async ({is}) => {
+  const expected = [
+    {
+      level: 'info',
+      msg: 'hello world'
+    },
+    {
+      level: 'foo',
+      msg: 'foobar'
+    }
+  ]
+  const opts = {
+    useLevelLabels: true,
+    customLevels: {
+      foo: 35
+    }
+  }
+  const instance = pino(opts, sink((result, enc, cb) => {
+    const current = expected.shift()
+    check(is, result, current.level, current.msg)
+    cb()
+  }))
+
+  instance.info('hello world')
+  instance.foo('foobar')
+})