packages/ecs-morgan-format/index.js (105 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
'use strict'
const morgan = require('morgan')
const safeStableStringify = require('safe-stable-stringify')
const {
version,
formatHttpRequest,
formatHttpResponse
} = require('@elastic/ecs-helpers')
// We will query the Elastic APM agent if it is available.
let elasticApm = null
try {
elasticApm = require('elastic-apm-node')
} catch (ex) {
// Silently ignore.
}
const stringify = safeStableStringify.configure({ deterministic: false })
// Return a Morgan formatter function for ecs-logging output.
//
// @param {Object} opts - Optional.
// - {String || Function} opts.format - A format *name* (e.g. 'combined'),
// format function (e.g. `morgan.combined`), or a format string
// (e.g. ':method :url :status'). This is used to format the "message"
// field. Defaults to `morgan.combined`.
// - {Boolean} opts.apmIntegration - Whether to automatically integrate with
// Elastic APM (https://github.com/elastic/apm-agent-nodejs). If a started
// APM agent is detected, then log records will include the following
// fields:
// - "service.name" - the configured serviceName in the agent
// - "event.dataset" - set to "$serviceName" for correlation in Kibana
// - "trace.id", "transaction.id", and "span.id" - if there is a current
// active trace when the log call is made
// Default true.
// - {String} serviceName - override `service.name` field from APM agent
// - {String} serviceVersion - override `service.version` field from APM agent
// - {String} serviceEnvironment - override `service.environment` field from APM agent
// - {String} serviceNodeName - override `service.name` field from APM agent
// - {String} eventDataset - override `event.dataset` field
//
// For backwards compatibility, the first argument can be a String or Function
// to specify `opts.format`. For example, the following are equivalent:
// ecsFormat({format: 'combined'})
// ecsFormat('combined')
// The former allows specifying other options.
function ecsFormat (opts) {
let format = morgan.combined
let apmIntegration = true
if (opts && typeof opts === 'object') {
// Usage: ecsFormat({ /* opts */ })
if (opts.format != null) {
format = opts.format
}
if (opts.apmIntegration != null) {
apmIntegration = opts.apmIntegration
}
} else if (opts) {
// Usage: ecsFormat(format)
format = opts
opts = {}
} else {
// Usage: ecsFormat()
opts = {}
}
// Resolve to a format function a la morgan's own `getFormatFunction`.
let fmt = morgan[format] || format
if (typeof fmt !== 'function') {
fmt = morgan.compile(fmt)
}
let apm = null
if (apmIntegration && elasticApm && elasticApm.isStarted && elasticApm.isStarted()) {
apm = elasticApm
}
const extraFields = {}
// 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) {
extraFields['service.name'] = serviceName
}
let serviceVersion = opts.serviceVersion
// istanbul ignore next
if (serviceVersion == null && apm) {
serviceVersion = (apm.getServiceVersion
? apm.getServiceVersion() // added in elastic-apm-node@...
: apm._conf.serviceVersion) // fallback to private `_conf`
}
if (serviceVersion) {
extraFields['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) {
extraFields['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) {
extraFields['service.node.name'] = serviceNodeName
}
let eventDataset = opts.eventDataset
if (eventDataset == null && serviceName) {
eventDataset = serviceName
}
if (eventDataset) {
extraFields['event.dataset'] = eventDataset
}
return function formatter (token, req, res) {
const ecsFields = {
'@timestamp': new Date().toISOString(),
'log.level': res.statusCode < 500 ? 'info' : 'error',
message: fmt(token, req, res),
'ecs.version': version,
...extraFields
}
// https://www.elastic.co/guide/en/ecs/current/ecs-tracing.html
if (apm) {
const tx = apm.currentTransaction
// istanbul ignore else
if (tx) {
ecsFields['trace.id'] = tx.traceId
ecsFields['transaction.id'] = tx.id
// Not including `span.id` because the way morgan logs (on the HTTP
// Response "finished" event), any spans during the request handler
// are no longer active.
}
}
// https://www.elastic.co/guide/en/ecs/current/ecs-http.html
formatHttpRequest(ecsFields, req)
formatHttpResponse(ecsFields, res)
return stringify(ecsFields)
}
}
// For backwards compatibility with v1.0.0, the top-level export is `ecsFormat`,
// though using the named export is preferred.
module.exports = ecsFormat
module.exports.ecsFormat = ecsFormat
module.exports.default = ecsFormat