Skip to content

Commit

Permalink
fix: add explicit dep for triple-beam, rather than getting lucky
Browse files Browse the repository at this point in the history
Document subtle diffs with our stringifier and winston.format.json().
Use a Format class to avoid needing a dep on winston or logform
for the 'winston.format(...)' call.

Closes: #108
  • Loading branch information
trentm committed Oct 19, 2023
1 parent 8878264 commit 626ab44
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 111 deletions.
7 changes: 7 additions & 0 deletions packages/ecs-winston-format/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
protects against circular references and bigints.
(https://github.com/elastic/ecs-logging-nodejs/pull/155)

- Explicitly depend on `triple-beam` (`>=1.1.0` when `MESSAGE` was added).
Before this change, this package was assuming that it would be installed by
the user. This worked for npm's flat install -- `npm install winston` will
install a `node_modules/triple-beam/...` -- but not for Yarn 2's PnP install
mechanism.
(https://github.com/elastic/ecs-logging-nodejs/issues/108)

## v1.4.0

- Add `service.version`, `service.environment`, and `service.node.name` log
Expand Down
235 changes: 125 additions & 110 deletions packages/ecs-winston-format/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
'use strict'

const { MESSAGE } = require('triple-beam')
const { format } = require('winston')
const safeStableStringify = require('safe-stable-stringify')
const {
version,
Expand All @@ -35,6 +34,17 @@ try {
// Silently ignore.
}

// There are some differences between Winston's `logform.format.json()` and this
// stringifier. They both use `safe-stable-stringify`. Winston's exposes its
// options but doesn't doc that at https://github.com/winstonjs/logform#json.
// 1. This one hardcodes `deterministic: false` so fields are serialized in the
// order added, which is helpful for ecs-logging's stated preference of
// having a few fields first:
// https://www.elastic.co/guide/en/ecs-logging/overview/current/intro.html#_why_ecs_logging
// 2. Winston provides a `replacer` that converts bigints to strings. Doing
// that is debatable. The argument *for* is that a *JavaScript* JSON parser
// looses precision when parsing a bigint.
// TODO: These differences should make it to docs somewhere.
const stringify = safeStableStringify.configure({ deterministic: false })

const reservedFields = {
Expand All @@ -48,135 +58,140 @@ const reservedFields = {
}

/**
* Create a Winston format for ecs-logging output.
* A Winston `Format` for converting to ecs-logging output.
*
* @param {import('logform').TransformableInfo} info
* @class {import('logform').Format}
* @param {Config} opts - See index.d.ts.
*/
function ecsTransform (info, opts) {
// istanbul ignore next
opts = opts || {}
const convertErr = opts.convertErr != null ? opts.convertErr : true
const convertReqRes = opts.convertReqRes != null ? opts.convertReqRes : false
const apmIntegration = opts.apmIntegration != null ? opts.apmIntegration : true

const ecsFields = {
'@timestamp': new Date().toISOString(),
'log.level': info.level,
message: info.message,
'ecs.version': version
class EcsWinstonTransform {
constructor (opts) {
this.options = opts
}
transform (info, opts) {
// istanbul ignore next
opts = opts || {}
const convertErr = opts.convertErr != null ? opts.convertErr : true
const convertReqRes = opts.convertReqRes != null ? opts.convertReqRes : false
const apmIntegration = opts.apmIntegration != null ? opts.apmIntegration : true

const ecsFields = {
'@timestamp': new Date().toISOString(),
'log.level': info.level,
message: info.message,
'ecs.version': version
}

// Add all unreserved fields.
const keys = Object.keys(info)
for (let i = 0, len = keys.length; i < len; i++) {
const key = keys[i]
if (!reservedFields[key]) {
ecsFields[key] = info[key]
// Add all unreserved fields.
const keys = Object.keys(info)
for (let i = 0, len = keys.length; i < len; i++) {
const key = keys[i]
if (!reservedFields[key]) {
ecsFields[key] = info[key]
}
}
}

let apm = null
if (apmIntegration && elasticApm && elasticApm.isStarted && elasticApm.isStarted()) {
apm = elasticApm
}
let apm = null
if (apmIntegration && elasticApm && elasticApm.isStarted && elasticApm.isStarted()) {
apm = elasticApm
}

// Set a number of correlation fields from (a) the given options or (b) an
// APM agent, if there is one running.
let serviceName = opts.serviceName
if (serviceName == null && apm) {
// istanbul ignore next
serviceName = (apm.getServiceName
? apm.getServiceName() // added in elastic-apm-node@3.11.0
: apm._conf.serviceName) // fallback to private `_conf`
}
if (serviceName) {
ecsFields['service.name'] = serviceName
}
// Set a number of correlation fields from (a) the given options or (b) an
// APM agent, if there is one running.
let serviceName = opts.serviceName
if (serviceName == null && apm) {
// istanbul ignore next
serviceName = (apm.getServiceName
? apm.getServiceName() // added in elastic-apm-node@3.11.0
: apm._conf.serviceName) // fallback to private `_conf`
}
if (serviceName) {
ecsFields['service.name'] = serviceName
}

let serviceVersion = opts.serviceVersion
if (serviceVersion == null && apm) {
// istanbul ignore next
serviceVersion = (apm.getServiceVersion
? apm.getServiceVersion() // added in elastic-apm-node@...
: apm._conf.serviceVersion) // fallback to private `_conf`
}
if (serviceVersion) {
ecsFields['service.version'] = serviceVersion
}
let serviceVersion = opts.serviceVersion
if (serviceVersion == null && apm) {
// istanbul ignore next
serviceVersion = (apm.getServiceVersion
? apm.getServiceVersion() // added in elastic-apm-node@...
: apm._conf.serviceVersion) // fallback to private `_conf`
}
if (serviceVersion) {
ecsFields['service.version'] = serviceVersion
}

let serviceEnvironment = opts.serviceEnvironment
if (serviceEnvironment == null && apm) {
// istanbul ignore next
serviceEnvironment = (apm.getServiceEnvironment
? apm.getServiceEnvironment() // added in elastic-apm-node@...
: apm._conf.environment) // fallback to private `_conf`
}
if (serviceEnvironment) {
ecsFields['service.environment'] = serviceEnvironment
}
let serviceEnvironment = opts.serviceEnvironment
if (serviceEnvironment == null && apm) {
// istanbul ignore next
serviceEnvironment = (apm.getServiceEnvironment
? apm.getServiceEnvironment() // added in elastic-apm-node@...
: apm._conf.environment) // fallback to private `_conf`
}
if (serviceEnvironment) {
ecsFields['service.environment'] = serviceEnvironment
}

let serviceNodeName = opts.serviceNodeName
if (serviceNodeName == null && apm) {
// istanbul ignore next
serviceNodeName = (apm.getServiceNodeName
? apm.getServiceNodeName() // added in elastic-apm-node@...
: apm._conf.serviceNodeName) // fallback to private `_conf`
}
if (serviceNodeName) {
ecsFields['service.node.name'] = serviceNodeName
}
let serviceNodeName = opts.serviceNodeName
if (serviceNodeName == null && apm) {
// istanbul ignore next
serviceNodeName = (apm.getServiceNodeName
? apm.getServiceNodeName() // added in elastic-apm-node@...
: apm._conf.serviceNodeName) // fallback to private `_conf`
}
if (serviceNodeName) {
ecsFields['service.node.name'] = serviceNodeName
}

let eventDataset = opts.eventDataset
if (eventDataset == null && serviceName) {
eventDataset = serviceName
}
if (eventDataset) {
ecsFields['event.dataset'] = eventDataset
}
let eventDataset = opts.eventDataset
if (eventDataset == null && serviceName) {
eventDataset = serviceName
}
if (eventDataset) {
ecsFields['event.dataset'] = eventDataset
}

// istanbul ignore else
if (apm) {
// https://www.elastic.co/guide/en/ecs/current/ecs-tracing.html
const tx = apm.currentTransaction
if (tx) {
ecsFields['trace.id'] = tx.traceId
ecsFields['transaction.id'] = tx.id
const span = apm.currentSpan
// istanbul ignore else
if (span) {
ecsFields['span.id'] = span.id
// istanbul ignore else
if (apm) {
// https://www.elastic.co/guide/en/ecs/current/ecs-tracing.html
const tx = apm.currentTransaction
if (tx) {
ecsFields['trace.id'] = tx.traceId
ecsFields['transaction.id'] = tx.id
const span = apm.currentSpan
// istanbul ignore else
if (span) {
ecsFields['span.id'] = span.id
}
}
}
}

// https://www.elastic.co/guide/en/ecs/current/ecs-error.html
if (info.err !== undefined) {
if (convertErr) {
formatError(ecsFields, info.err)
} else {
ecsFields.err = info.err
// https://www.elastic.co/guide/en/ecs/current/ecs-error.html
if (info.err !== undefined) {
if (convertErr) {
formatError(ecsFields, info.err)
} else {
ecsFields.err = info.err
}
}
}

// https://www.elastic.co/guide/en/ecs/current/ecs-http.html
if (info.req !== undefined) {
if (convertReqRes) {
formatHttpRequest(ecsFields, info.req)
} else {
ecsFields.req = info.req
// https://www.elastic.co/guide/en/ecs/current/ecs-http.html
if (info.req !== undefined) {
if (convertReqRes) {
formatHttpRequest(ecsFields, info.req)
} else {
ecsFields.req = info.req
}
}
}
if (info.res !== undefined) {
if (convertReqRes) {
formatHttpResponse(ecsFields, info.res)
} else {
ecsFields.res = info.res
if (info.res !== undefined) {
if (convertReqRes) {
formatHttpResponse(ecsFields, info.res)
} else {
ecsFields.res = info.res
}
}
}

info[MESSAGE] = stringify(ecsFields)
return info
info[MESSAGE] = stringify(ecsFields)
return info
}
}

module.exports = format(ecsTransform)
module.exports = opts => new EcsWinstonTransform(opts)
3 changes: 2 additions & 1 deletion packages/ecs-winston-format/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
},
"dependencies": {
"@elastic/ecs-helpers": "^2.0.0",
"safe-stable-stringify": "^2.4.3"
"safe-stable-stringify": "^2.4.3",
"triple-beam": ">=1.1.0"
},
"devDependencies": {
"ajv": "^7.0.3",
Expand Down

0 comments on commit 626ab44

Please sign in to comment.