@@ -67,130 +67,173 @@ function createEcsPinoOptions (opts) {
67
67
}
68
68
69
69
let apm = null
70
+ let apmServiceName = null
70
71
if ( apmIntegration && elasticApm && elasticApm . isStarted && elasticApm . isStarted ( ) ) {
71
72
apm = elasticApm
73
+ // Elastic APM v3.11.0 added getServiceName(). Fallback to private `apm._conf`.
74
+ // istanbul ignore next
75
+ apmServiceName = apm . getServiceName
76
+ ? apm . getServiceName ( )
77
+ : apm . _conf . serviceName
72
78
}
73
79
80
+ let isServiceNameInBindings = false
81
+ let isEventDatasetInBindings = false
82
+
74
83
const ecsPinoOptions = {
84
+ messageKey : 'message' ,
85
+ timestamp : ( ) => `,"@timestamp":"${ new Date ( ) . toISOString ( ) } "` ,
75
86
formatters : {
76
87
level ( label , number ) {
77
88
return { 'log.level' : label }
78
89
} ,
79
90
80
- // Add the following ECS fields:
81
- // - https://www.elastic.co/guide/en/ecs/current/ecs-process.html#field-process-pid
82
- // - https://www.elastic.co/guide/en/ecs/current/ecs-host.html#field-host-hostname
83
- // - https://www.elastic.co/guide/en/ecs/current/ecs-log.html#field-log-logger
84
- //
85
- // This is called once at logger creation, and for each child logger creation.
86
91
bindings ( bindings ) {
87
92
const {
88
- // We assume the default `pid` and `hostname` bindings
89
- // (https://getpino.io/#/docs/api?id=bindings) will be always be
90
- // defined because currently one cannot use this package *and*
91
- // pass a custom `formatters` to a pino logger.
93
+ // `pid` and `hostname` are default bindings, unless overriden by
94
+ // a `base: {...}` passed to logger creation.
92
95
pid,
93
96
hostname,
94
97
// name is defined if `log = pino({name: 'my name', ...})`
95
- name
98
+ name,
99
+ // Warning: silently drop any "ecs" value from `base`. See
100
+ // "ecs.version" comment below.
101
+ ecs,
102
+ ...ecsBindings
96
103
} = bindings
97
104
98
- const ecsBindings = {
99
- ecs : {
100
- version
101
- } ,
102
- process : {
103
- pid : pid
104
- } ,
105
- host : {
106
- hostname : hostname
107
- }
105
+ if ( pid !== undefined ) {
106
+ // https://www.elastic.co/guide/en/ecs/current/ecs-process.html#field-process-pid
107
+ ecsBindings . process = { pid : pid }
108
+ }
109
+ if ( hostname !== undefined ) {
110
+ // https://www.elastic.co/guide/en/ecs/current/ecs-host.html#field-host-hostname
111
+ ecsBindings . host = { hostname : hostname }
108
112
}
109
113
if ( name !== undefined ) {
114
+ // https://www.elastic.co/guide/en/ecs/current/ecs-log.html#field-log-logger
110
115
ecsBindings . log = { logger : name }
111
116
}
112
117
118
+ // Note if service.name & event.dataset are set, to not do so again below.
119
+ if ( bindings . service && bindings . service . name ) {
120
+ isServiceNameInBindings = true
121
+ }
122
+ if ( bindings . event && bindings . event . dataset ) {
123
+ isEventDatasetInBindings = true
124
+ }
125
+
126
+ return ecsBindings
127
+ } ,
128
+
129
+ log ( obj ) {
130
+ const {
131
+ req,
132
+ res,
133
+ err,
134
+ ...ecsObj
135
+ } = obj
136
+
137
+ // https://www.elastic.co/guide/en/ecs/current/ecs-ecs.html
138
+ // For "ecs.version" we take a heavier-handed approach, because it is
139
+ // a require ecs-logging field: overwrite any possible "ecs" value from
140
+ // the log statement. This means we don't need to spend the time
141
+ // guarding against "ecs" being null, Array, Buffer, Date, etc.
142
+ ecsObj . ecs = { version }
143
+
113
144
if ( apm ) {
114
- // https://github.com/elastic/apm-agent-nodejs/pull/1949 is adding
115
- // getServiceName() in v3.11.0. Fallback to private `apm._conf`.
116
- // istanbul ignore next
117
- const serviceName = apm . getServiceName
118
- ? apm . getServiceName ( )
119
- : apm . _conf . serviceName
120
145
// A mis-configured APM Agent can be "started" but not have a
121
146
// "serviceName".
122
- if ( serviceName ) {
123
- ecsBindings . service = { name : serviceName }
124
- ecsBindings . event = { dataset : serviceName + '.log' }
147
+ if ( apmServiceName ) {
148
+ // Per https://github.com/elastic/ecs-logging/blob/master/spec/spec.json
149
+ // "service.name" and "event.dataset" should be automatically set
150
+ // if not already by the user.
151
+ if ( ! isServiceNameInBindings ) {
152
+ const service = ecsObj . service
153
+ if ( service === undefined ) {
154
+ ecsObj . service = { name : apmServiceName }
155
+ } else if ( ! isVanillaObject ( service ) ) {
156
+ // Warning: "service" type conflicts with ECS spec. Overwriting.
157
+ ecsObj . service = { name : apmServiceName }
158
+ } else if ( typeof service . name !== 'string' ) {
159
+ ecsObj . service . name = apmServiceName
160
+ }
161
+ }
162
+ if ( ! isEventDatasetInBindings ) {
163
+ const event = ecsObj . event
164
+ if ( event === undefined ) {
165
+ ecsObj . event = { dataset : apmServiceName + '.log' }
166
+ } else if ( ! isVanillaObject ( event ) ) {
167
+ // Warning: "event" type conflicts with ECS spec. Overwriting.
168
+ ecsObj . event = { dataset : apmServiceName + '.log' }
169
+ } else if ( typeof event . dataset !== 'string' ) {
170
+ ecsObj . event . dataset = apmServiceName + '.log'
171
+ }
172
+ }
125
173
}
126
- }
127
-
128
- return ecsBindings
129
- }
130
- } ,
131
- messageKey : 'message' ,
132
- timestamp : ( ) => `,"@timestamp":"${ new Date ( ) . toISOString ( ) } "`
133
- }
134
174
135
- // For performance, avoid adding the `formatters.log` pino option unless we
136
- // know we'll do some processing in it.
137
- if ( convertErr || convertReqRes || apm ) {
138
- ecsPinoOptions . formatters . log = function ( obj ) {
139
- const {
140
- req,
141
- res,
142
- err,
143
- ...ecsObj
144
- } = obj
145
-
146
- // istanbul ignore else
147
- if ( apm ) {
148
- // https://www.elastic.co/guide/en/ecs/current/ecs-tracing.html
149
- const tx = apm . currentTransaction
150
- if ( tx ) {
151
- ecsObj . trace = ecsObj . trace || { }
152
- ecsObj . trace . id = tx . traceId
153
- ecsObj . transaction = ecsObj . transaction || { }
154
- ecsObj . transaction . id = tx . id
155
- const span = apm . currentSpan
156
- // istanbul ignore else
157
- if ( span ) {
158
- ecsObj . span = ecsObj . span || { }
159
- ecsObj . span . id = span . id
175
+ // https://www.elastic.co/guide/en/ecs/current/ecs-tracing.html
176
+ const tx = apm . currentTransaction
177
+ if ( tx ) {
178
+ ecsObj . trace = ecsObj . trace || { }
179
+ ecsObj . trace . id = tx . traceId
180
+ ecsObj . transaction = ecsObj . transaction || { }
181
+ ecsObj . transaction . id = tx . id
182
+ const span = apm . currentSpan
183
+ // istanbul ignore else
184
+ if ( span ) {
185
+ ecsObj . span = ecsObj . span || { }
186
+ ecsObj . span . id = span . id
187
+ }
160
188
}
161
189
}
162
- }
163
190
164
- // https://www.elastic.co/guide/en/ecs/current/ecs-http.html
165
- if ( err !== undefined ) {
166
- if ( ! convertErr ) {
167
- ecsObj . err = err
168
- } else {
169
- formatError ( ecsObj , err )
191
+ // https://www.elastic.co/guide/en/ecs/current/ecs-http.html
192
+ if ( err !== undefined ) {
193
+ if ( ! convertErr ) {
194
+ ecsObj . err = err
195
+ } else {
196
+ formatError ( ecsObj , err )
197
+ }
170
198
}
171
- }
172
199
173
- // https://www.elastic.co/guide/en/ecs/current/ecs-http.html
174
- if ( req !== undefined ) {
175
- if ( ! convertReqRes ) {
176
- ecsObj . req = req
177
- } else {
178
- formatHttpRequest ( ecsObj , req )
200
+ // https://www.elastic.co/guide/en/ecs/current/ecs-http.html
201
+ if ( req !== undefined ) {
202
+ if ( ! convertReqRes ) {
203
+ ecsObj . req = req
204
+ } else {
205
+ formatHttpRequest ( ecsObj , req )
206
+ }
179
207
}
180
- }
181
- if ( res !== undefined ) {
182
- if ( ! convertReqRes ) {
183
- ecsObj . res = res
184
- } else {
185
- formatHttpResponse ( ecsObj , res )
208
+ if ( res !== undefined ) {
209
+ if ( ! convertReqRes ) {
210
+ ecsObj . res = res
211
+ } else {
212
+ formatHttpResponse ( ecsObj , res )
213
+ }
186
214
}
187
- }
188
215
189
- return ecsObj
216
+ return ecsObj
217
+ }
190
218
}
191
219
}
192
220
193
221
return ecsPinoOptions
194
222
}
195
223
224
+ // Return true if the given arg is a "vanilla" object. Roughly the intent is
225
+ // whether this is basic mapping of string keys to values that will serialize
226
+ // as a JSON object.
227
+ //
228
+ // Currently, it excludes Map. The uses above don't really expect a user to:
229
+ // service = new Map([["foo", "bar"]])
230
+ // log.info({ service }, '...')
231
+ //
232
+ // There are many ways tackle this. See some attempts and benchmarks at:
233
+ // https://gist.github.com/trentm/34131a92eede80fd2109f8febaa56f5a
234
+ function isVanillaObject ( o ) {
235
+ return ( typeof o === 'object' &&
236
+ ( ! o . constructor || o . constructor . name === 'Object' ) )
237
+ }
238
+
196
239
module . exports = createEcsPinoOptions
0 commit comments