Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ if (cmd.h || cmd.help) {
// Remove default values
opts = filter(opts, value => value !== DEFAULT_VALUE)
const config = loadConfig(opts.config)
// Override config with cli options
opts = Object.assign({}, config, opts)
const envConfig = loadEnvConfig()
// Priority: CLI flags > config file > env vars
opts = Object.assign({}, envConfig, config, opts)
// set defaults
opts.errorLikeObjectKeys = opts.errorLikeObjectKeys || 'err,error'
opts.errorProps = opts.errorProps || ''
Expand All @@ -101,6 +102,56 @@ if (cmd.h || cmd.help) {
return result.data
}

/**
* Load pino-pretty configuration from environment variables.
* All options use the `PINO_PRETTY_` prefix and SCREAMING_SNAKE_CASE naming.
* For example: `--singleLine` → `PINO_PRETTY_SINGLE_LINE=true`
* Boolean options accept: `true`, `1`, `false`, `0` (case-insensitive).
* @returns {object}
*/
function loadEnvConfig () {
const envMap = {
PINO_PRETTY_COLORIZE: { key: 'colorize', type: 'boolean' },
PINO_PRETTY_CRLF: { key: 'crlf', type: 'boolean' },
PINO_PRETTY_ERROR_PROPS: { key: 'errorProps', type: 'string' },
PINO_PRETTY_LEVEL_FIRST: { key: 'levelFirst', type: 'boolean' },
PINO_PRETTY_MINIMUM_LEVEL: { key: 'minimumLevel', type: 'string' },
PINO_PRETTY_CUSTOM_LEVELS: { key: 'customLevels', type: 'string' },
PINO_PRETTY_CUSTOM_COLORS: { key: 'customColors', type: 'string' },
PINO_PRETTY_USE_ONLY_CUSTOM_PROPS: { key: 'useOnlyCustomProps', type: 'boolean' },
PINO_PRETTY_ERROR_LIKE_OBJECT_KEYS: { key: 'errorLikeObjectKeys', type: 'string' },
PINO_PRETTY_MESSAGE_KEY: { key: 'messageKey', type: 'string' },
PINO_PRETTY_LEVEL_KEY: { key: 'levelKey', type: 'string' },
PINO_PRETTY_LEVEL_LABEL: { key: 'levelLabel', type: 'string' },
PINO_PRETTY_MESSAGE_FORMAT: { key: 'messageFormat', type: 'string' },
PINO_PRETTY_TIMESTAMP_KEY: { key: 'timestampKey', type: 'string' },
PINO_PRETTY_TRANSLATE_TIME: { key: 'translateTime', type: 'string' },
PINO_PRETTY_IGNORE: { key: 'ignore', type: 'string' },
PINO_PRETTY_INCLUDE: { key: 'include', type: 'string' },
PINO_PRETTY_HIDE_OBJECT: { key: 'hideObject', type: 'boolean' },
PINO_PRETTY_SINGLE_LINE: { key: 'singleLine', type: 'boolean' }
}

const result = {}
for (const [envKey, { key, type }] of Object.entries(envMap)) {
const value = process.env[envKey]
if (value === undefined || value === '') continue

if (type === 'boolean') {
const lower = value.toLowerCase()
if (lower === 'true' || lower === '1') {
result[key] = true
} else if (lower === 'false' || lower === '0') {
result[key] = false
}
// Invalid boolean values are silently ignored
} else {
result[key] = value
}
}
return result
}

function filter (obj, cb) {
return Object.keys(obj).reduce((acc, key) => {
const value = obj[key]
Expand Down
114 changes: 114 additions & 0 deletions test/cli-env.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use strict'

process.env.TZ = 'UTC'

const path = require('node:path')
const { spawn } = require('node:child_process')
const { describe, test } = require('node:test')
const { once } = require('./helper')

const bin = require.resolve(path.join(__dirname, '..', 'bin.js'))
const logLine = '{"level":30,"time":1522431328992,"msg":"hello world","pid":42,"hostname":"foo"}\n'
const baseEnv = { TERM: 'dumb', TZ: 'UTC' }
const formattedEpoch = '17:35:28.992'

describe('cli env vars', () => {
test('PINO_PRETTY_LEVEL_FIRST=true flips level and time', async (t) => {
t.plan(1)
const env = { ...baseEnv, PINO_PRETTY_LEVEL_FIRST: 'true' }
const child = spawn(process.argv[0], [bin], { env })
child.on('error', t.assert.fail)
const endPromise = once(child.stdout, 'data', (data) => {
t.assert.strictEqual(data.toString(), `INFO [${formattedEpoch}] (42): hello world\n`)
})
child.stdin.write(logLine)
await endPromise
t.after(() => child.kill())
})

test('PINO_PRETTY_LEVEL_FIRST=1 flips level and time', async (t) => {
t.plan(1)
const env = { ...baseEnv, PINO_PRETTY_LEVEL_FIRST: '1' }
const child = spawn(process.argv[0], [bin], { env })
child.on('error', t.assert.fail)
const endPromise = once(child.stdout, 'data', (data) => {
t.assert.strictEqual(data.toString(), `INFO [${formattedEpoch}] (42): hello world\n`)
})
child.stdin.write(logLine)
await endPromise
t.after(() => child.kill())
})

test('PINO_PRETTY_SINGLE_LINE=true prints all on one line', async (t) => {
t.plan(1)
const multiKeyLog = '{"level":30,"time":1522431328992,"msg":"hello","pid":42,"hostname":"foo","extra":"value"}\n'
const env = { ...baseEnv, PINO_PRETTY_SINGLE_LINE: 'true' }
const child = spawn(process.argv[0], [bin], { env })
child.on('error', t.assert.fail)
const endPromise = once(child.stdout, 'data', (data) => {
// In single line mode the extra fields appear inline, not on separate lines
const output = data.toString()
t.assert.ok(!output.includes('\n '), 'should not have indented extra fields')
})
child.stdin.write(multiKeyLog)
await endPromise
t.after(() => child.kill())
})

test('PINO_PRETTY_IGNORE=pid,hostname removes those keys', async (t) => {
t.plan(1)
const env = { ...baseEnv, PINO_PRETTY_IGNORE: 'pid,hostname' }
const child = spawn(process.argv[0], [bin], { env })
child.on('error', t.assert.fail)
const endPromise = once(child.stdout, 'data', (data) => {
t.assert.strictEqual(data.toString(), `[${formattedEpoch}] INFO: hello world\n`)
})
child.stdin.write(logLine)
await endPromise
t.after(() => child.kill())
})

test('PINO_PRETTY_MESSAGE_KEY overrides default message key', async (t) => {
t.plan(1)
const customLog = '{"level":30,"time":1522431328992,"message":"custom key msg","pid":42,"hostname":"foo"}\n'
const env = { ...baseEnv, PINO_PRETTY_MESSAGE_KEY: 'message' }
const child = spawn(process.argv[0], [bin], { env })
child.on('error', t.assert.fail)
const endPromise = once(child.stdout, 'data', (data) => {
t.assert.ok(data.toString().includes('custom key msg'))
})
child.stdin.write(customLog)
await endPromise
t.after(() => child.kill())
})

test('CLI flag overrides env var (CLI wins)', async (t) => {
t.plan(1)
// env says level-first=true, but CLI does not pass it => CLI default should win
// Since the default is no levelFirst, output should show time first
const env = { ...baseEnv, PINO_PRETTY_LEVEL_FIRST: 'false' }
const child = spawn(process.argv[0], [bin, '--levelFirst'], { env })
child.on('error', t.assert.fail)
const endPromise = once(child.stdout, 'data', (data) => {
// CLI flag wins: level appears first
t.assert.strictEqual(data.toString(), `INFO [${formattedEpoch}] (42): hello world\n`)
})
child.stdin.write(logLine)
await endPromise
t.after(() => child.kill())
})

test('invalid PINO_PRETTY_LEVEL_FIRST value is silently ignored', async (t) => {
t.plan(1)
const env = { ...baseEnv, PINO_PRETTY_LEVEL_FIRST: 'yes' }
const child = spawn(process.argv[0], [bin], { env })
child.on('error', t.assert.fail)
const endPromise = once(child.stdout, 'data', (data) => {
// Default output: time first
t.assert.strictEqual(data.toString(), `[${formattedEpoch}] INFO (42): hello world\n`)
})
child.stdin.write(logLine)
await endPromise
t.after(() => child.kill())
})
})
Loading