Skip to content

Commit 1d684bd

Browse files
authored
feat(helpers): support hapi request object to http formatters (#51)
1 parent df58101 commit 1d684bd

File tree

5 files changed

+193
-38
lines changed

5 files changed

+193
-38
lines changed

helpers/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Add support for the hapi request object being passed to `formatHttpRequest`
6+
and `formatHttpResponse`.
57
- Fix the setting of the remote IP and port
68
[ECS client fields](https://www.elastic.co/guide/en/ecs/current/ecs-client.html):
79
`client.address`, `client.ip`, `client.port`. This also supports using

helpers/README.md

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ of objects with circular references. This generally means that ecs-logging-nodej
4646
libraries will throw a "Converting circular structure to JSON" exception if an
4747
attempt is made to log an object with circular references.
4848

49-
### `formatError`
49+
### `formatError(obj, err)`
5050

5151
A function that adds [ECS Error fields](https://www.elastic.co/guide/en/ecs/current/ecs-error.html)
5252
for a given `Error` object.
@@ -79,56 +79,93 @@ metadata field passed to a logging statement. E.g.
7979
`log.warn({err: myErr}, '...')` for pino, `log.warn('...', {err: myErr})`
8080
for winston.
8181

82-
### `formatHttpRequest`
82+
### `formatHttpRequest(obj, req)`
8383

8484
Function that enhances an ECS object with http request data.
85-
The request object should be Node.js's core
86-
[`http.IncomingMessage`](https://nodejs.org/api/all.html#http_class_http_incomingmessage),
87-
or [Express's request object](https://expressjs.com/en/5x/api.html#req) that
88-
extends it.
85+
The given request object, `req`, must be one of the following:
86+
- Node.js's core [`http.IncomingMessage`](https://nodejs.org/api/all.html#http_class_http_incomingmessage),
87+
- [Express's request object](https://expressjs.com/en/5x/api.html#req) that extends IncomingMessage, or
88+
- a [hapi request object](https://hapi.dev/api/#request).
8989

9090
```js
91+
const http = require('http')
9192
const { formatHttpRequest } = require('@elastic/ecs-helpers')
92-
const ecs = {
93-
'@timestamp': new Date().toISOString(),
94-
'log.level': 'info',
95-
message: 'hello world',
96-
log: {
97-
logger: 'test'
98-
},
99-
ecs: {
100-
version: '1.4.0'
101-
}
102-
}
10393

104-
formatHttpRequest(ecs, request)
105-
console.log(ecs)
94+
http.createServer(function (req, res) {
95+
res.end('hi')
96+
97+
const obj = {}
98+
formatHttpRequest(obj, req)
99+
console.log('obj:', JSON.stringify(obj, null, 4))
100+
}).listen(3000)
106101
```
107102

108-
### `formatHttpResponse`
103+
Running this and making a request via `curl http://localhost:3000/` will
104+
print something close to:
105+
106+
```
107+
obj: {
108+
"http": {
109+
"version": "1.1",
110+
"request": {
111+
"method": "get",
112+
"headers": {
113+
"host": "localhost:3000",
114+
"accept": "*/*"
115+
}
116+
}
117+
},
118+
"url": {
119+
"full": "http://localhost:3000/",
120+
"path": "/"
121+
},
122+
"client": {
123+
"address": "::1",
124+
"ip": "::1",
125+
"port": 61969
126+
},
127+
"user_agent": {
128+
"original": "curl/7.64.1"
129+
}
130+
}
131+
```
132+
133+
### `formatHttpResponse(obj, res)`
109134

110135
Function that enhances an ECS object with http response data.
111-
The response object should be Node.js's core
112-
[`http.ServerResponse`](https://nodejs.org/api/all.html#http_class_http_serverresponse),
113-
or [Express's response object](https://expressjs.com/en/5x/api.html#res) that
114-
extends it.
136+
The given request object, `req`, must be one of the following:
137+
- Node.js's core [`http.ServerResponse`](https://nodejs.org/api/all.html#http_class_http_serverresponse),
138+
- [Express's response object](https://expressjs.com/en/5x/api.html#res) that extends ServerResponse, or
139+
- a [hapi **request** object](https://hapi.dev/api/#request)
115140

116141
```js
117-
const { formatHttpResponse } = require('@elastic/ecs-helpers')
118-
const ecs = {
119-
'@timestamp': new Date().toISOString(),
120-
'log.level': 'info',
121-
message: 'hello world',
122-
log: {
123-
logger: 'test'
124-
},
125-
ecs: {
126-
version: '1.4.0'
127-
}
128-
}
142+
const http = require('http')
143+
const { formatHttpRequest } = require('@elastic/ecs-helpers')
144+
145+
http.createServer(function (req, res) {
146+
res.setHeader('Foo', 'Bar')
147+
res.end('hi')
148+
149+
const obj = {}
150+
formatHttpResponse(obj, res)
151+
console.log('obj:', JSON.stringify(obj, null, 4))
152+
}).listen(3000)
153+
```
154+
155+
Running this and making a request via `curl http://localhost:3000/` will
156+
print something close to:
129157

130-
formatHttpResponse(ecs, request)
131-
console.log(ecs)
158+
```
159+
rec: {
160+
"http": {
161+
"response": {
162+
"status_code": 200,
163+
"headers": {
164+
"foo": "Bar"
165+
}
166+
}
167+
}
168+
}
132169
```
133170

134171
## License

helpers/lib/http-formatters.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818
'use strict'
1919

2020
function formatHttpRequest (ecs, req) {
21+
if (req.raw && req.raw.req && req.raw.req.httpVersion) {
22+
// This looks like a hapi request object (https://hapi.dev/api/#request),
23+
// use the raw Node.js http.IncomingMessage that it references.
24+
// TODO: Use hapi's already parsed `req.url` for speed.
25+
req = req.raw.req
26+
}
27+
2128
const {
2229
id,
2330
method,
@@ -100,6 +107,12 @@ function formatHttpRequest (ecs, req) {
100107
}
101108

102109
function formatHttpResponse (ecs, res) {
110+
if (res.raw && res.raw.res && typeof (res.raw.res.getHeaders) === 'function') {
111+
// This looks like a hapi request object (https://hapi.dev/api/#request),
112+
// use the raw Node.js http.ServerResponse that it references.
113+
res = res.raw.res
114+
}
115+
103116
const { statusCode } = res
104117
ecs.http = ecs.http || {}
105118
ecs.http.response = ecs.http.response || {}

helpers/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"fast-json-stringify": "^2.4.1"
2727
},
2828
"devDependencies": {
29+
"@hapi/hapi": "^20.1.0",
2930
"ajv": "^7.0.3",
3031
"ajv-formats": "^1.5.1",
3132
"express": "^4.17.1",

helpers/test/hapi.test.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
'use strict'
19+
20+
const http = require('http')
21+
22+
const semver = require('semver')
23+
const test = require('tap').test
24+
const hapiNodeEngines = require('@hapi/hapi/package.json').engines.node
25+
26+
const {
27+
formatHttpRequest,
28+
formatHttpResponse
29+
} = require('../')
30+
31+
const testOpts = {
32+
skip: !semver.satisfies(process.versions.node, hapiNodeEngines) &&
33+
`node ${process.version} is not supported by this hapi (${hapiNodeEngines})`
34+
}
35+
36+
test('hapi res/req serialization', testOpts, t => {
37+
const Hapi = require('@hapi/hapi')
38+
const server = Hapi.server({ host: 'localhost' })
39+
40+
server.route({
41+
method: 'GET',
42+
path: '/',
43+
handler: (request, h) => {
44+
return h.response('hi')
45+
.header('Foo', 'Bar')
46+
}
47+
})
48+
49+
server.events.on('response', (request) => {
50+
const rec = {}
51+
formatHttpRequest(rec, request)
52+
formatHttpResponse(rec, request)
53+
54+
t.deepEqual(rec.user_agent, { original: 'cool-agent' })
55+
t.deepEqual(rec.url, {
56+
path: '/',
57+
full: `http://localhost:${server.info.port}/`
58+
})
59+
t.deepEqual(rec.http, {
60+
version: '1.1',
61+
request: {
62+
method: 'get',
63+
headers: {
64+
host: `localhost:${server.info.port}`,
65+
connection: 'close'
66+
}
67+
},
68+
response: {
69+
status_code: 200,
70+
headers: {
71+
foo: 'Bar',
72+
'content-type': 'text/html; charset=utf-8',
73+
'cache-control': 'no-cache',
74+
'accept-ranges': 'bytes'
75+
},
76+
body: {
77+
bytes: 2
78+
}
79+
}
80+
})
81+
// https://www.elastic.co/guide/en/ecs/current/ecs-client.html fields
82+
t.ok(rec.client, 'client fields are set')
83+
t.ok(rec.client.address === '127.0.0.1' || rec.client.address === '::ffff:127.0.0.1',
84+
'client.address is set')
85+
t.ok(rec.client.ip === rec.client.address,
86+
'client.address duplicated to client.ip')
87+
t.equal(typeof (rec.client.port), 'number')
88+
89+
server.stop().then(function () {
90+
t.end()
91+
})
92+
})
93+
94+
server.start().then(function () {
95+
t.comment('hapi server running on %s', server.info.uri)
96+
97+
// Make a request so we trigger a 'response' event above.
98+
const req = http.get(`http://localhost:${server.info.port}/`,
99+
{ headers: { 'user-agent': 'cool-agent' } })
100+
req.on('error', t.ifErr)
101+
})
102+
})

0 commit comments

Comments
 (0)