@defra/lis-infra-ui-services 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/lis-infra-ui-services",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "UI-Services shared definitions for the livestock taxonomy topology.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -54,7 +54,6 @@
|
|
|
54
54
|
"author": "Defra DDTS",
|
|
55
55
|
"license": "OGL-UK-3.0",
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@defra/hapi-tracing": "1.30.0",
|
|
58
57
|
"@elastic/ecs-pino-format": "1.5.0",
|
|
59
58
|
"@hapi/catbox-memory": "6.0.2",
|
|
60
59
|
"@hapi/catbox-redis": "7.0.2",
|
|
@@ -73,9 +72,11 @@
|
|
|
73
72
|
"undici": "7.24.8"
|
|
74
73
|
},
|
|
75
74
|
"peerDependencies": {
|
|
76
|
-
"eslint": "^9.0.0"
|
|
75
|
+
"eslint": "^9.0.0",
|
|
76
|
+
"@defra/lis-hubs-infra-core": "^0.2.0"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
+
"@defra/lis-hubs-infra-core": "^0.2.0",
|
|
79
80
|
"@defra/lis-infra-eslint-config": "^0.6.0",
|
|
80
81
|
"@vitest/coverage-istanbul": "^4.1.10",
|
|
81
82
|
"audit-ci": "^7.1.0",
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import { createRequire } from 'node:module'
|
|
2
2
|
|
|
3
3
|
import { ecsFormat } from '@elastic/ecs-pino-format'
|
|
4
|
-
import {
|
|
4
|
+
import { requestContext } from '@defra/lis-hubs-infra-core'
|
|
5
5
|
|
|
6
6
|
const require = createRequire(import.meta.url)
|
|
7
7
|
const pinoPretty = require('pino-pretty')
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Wraps ecs-pino-format's log formatter to also surface error.cause.
|
|
11
|
+
* ECS's error.* fields (type, message, stack_trace) predate Error.cause
|
|
12
|
+
* (ES2022) and have no field for it, so ecs-pino-format silently drops it -
|
|
13
|
+
* this puts it back as error.cause when present.
|
|
14
|
+
* @param {object} ecs - The object returned by ecsFormat().
|
|
15
|
+
* @returns {Function}
|
|
16
|
+
*/
|
|
17
|
+
function createEcsLogFormatter(ecs) {
|
|
18
|
+
return function log(obj) {
|
|
19
|
+
const formatted = ecs.formatters.log(obj)
|
|
20
|
+
const cause = obj.err?.cause
|
|
21
|
+
|
|
22
|
+
if (cause !== undefined && formatted.error) {
|
|
23
|
+
formatted.error.cause = cause?.message ?? cause?.code ?? cause
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return formatted
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
9
30
|
/**
|
|
10
31
|
* @param {{ logConfig: object, serviceName: string, serviceVersion: string }} options
|
|
11
32
|
* @returns {object}
|
|
@@ -15,12 +36,15 @@ export function createLoggerOptions({
|
|
|
15
36
|
serviceName,
|
|
16
37
|
serviceVersion
|
|
17
38
|
}) {
|
|
39
|
+
const ecs = ecsFormat({ serviceVersion, serviceName })
|
|
40
|
+
|
|
18
41
|
const formatters = {
|
|
19
42
|
ecs: {
|
|
20
|
-
...
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
...ecs,
|
|
44
|
+
formatters: {
|
|
45
|
+
...ecs.formatters,
|
|
46
|
+
log: createEcsLogFormatter(ecs)
|
|
47
|
+
}
|
|
24
48
|
},
|
|
25
49
|
json: {},
|
|
26
50
|
'pino-pretty': {
|
|
@@ -42,10 +66,10 @@ export function createLoggerOptions({
|
|
|
42
66
|
nesting: true,
|
|
43
67
|
mixin() {
|
|
44
68
|
const mixinValues = {}
|
|
45
|
-
const
|
|
69
|
+
const correlationId = requestContext.get('correlation_id')
|
|
46
70
|
|
|
47
|
-
if (
|
|
48
|
-
mixinValues.trace = { id:
|
|
71
|
+
if (correlationId) {
|
|
72
|
+
mixinValues.trace = { id: correlationId }
|
|
49
73
|
}
|
|
50
74
|
|
|
51
75
|
return mixinValues
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { requestContext } from '@defra/lis-hubs-infra-core'
|
|
4
|
+
import { createLoggerOptions } from './logger-options.js'
|
|
5
|
+
|
|
6
|
+
vi.mock('@defra/lis-hubs-infra-core')
|
|
7
|
+
|
|
8
|
+
const mocks = {
|
|
9
|
+
requestContextGet: vi.mocked(requestContext.get)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createOptions(format = 'ecs') {
|
|
13
|
+
return createLoggerOptions({
|
|
14
|
+
logConfig: { enabled: true, level: 'info', format, redact: [] },
|
|
15
|
+
serviceName: 'test-service',
|
|
16
|
+
serviceVersion: '1.2.3'
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('createLoggerOptions()', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('mixin adds trace.id from the request context correlation id', () => {
|
|
26
|
+
// Arrange
|
|
27
|
+
mocks.requestContextGet.mockReturnValue('correlation-123')
|
|
28
|
+
|
|
29
|
+
// Act
|
|
30
|
+
const mixinValues = createOptions().mixin()
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect(mixinValues).toEqual({ trace: { id: 'correlation-123' } })
|
|
34
|
+
expect(mocks.requestContextGet).toHaveBeenCalledWith('correlation_id')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('mixin omits trace when no correlation id is available', () => {
|
|
38
|
+
// Arrange
|
|
39
|
+
mocks.requestContextGet.mockReturnValue(null)
|
|
40
|
+
|
|
41
|
+
// Act
|
|
42
|
+
const mixinValues = createOptions().mixin()
|
|
43
|
+
|
|
44
|
+
// Assert
|
|
45
|
+
expect(mixinValues).toEqual({})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('ecs log formatter preserves error.cause alongside the standard ECS fields', () => {
|
|
49
|
+
// Arrange
|
|
50
|
+
const cause = new Error('getaddrinfo ENOTFOUND identity.example')
|
|
51
|
+
const err = new Error('fetch failed')
|
|
52
|
+
err.cause = cause
|
|
53
|
+
|
|
54
|
+
// Act
|
|
55
|
+
const formatted = createOptions().formatters.log({ err, msg: 'failed' })
|
|
56
|
+
|
|
57
|
+
// Assert
|
|
58
|
+
expect(formatted.error.message).toBe('fetch failed')
|
|
59
|
+
expect(formatted.error.type).toBe('Error')
|
|
60
|
+
expect(formatted.error.cause).toBe(cause.message)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('ecs log formatter falls back to cause.code when cause has no message', () => {
|
|
64
|
+
// Arrange
|
|
65
|
+
const cause = { code: 'ENOTFOUND' }
|
|
66
|
+
const err = new Error('fetch failed')
|
|
67
|
+
err.cause = cause
|
|
68
|
+
|
|
69
|
+
// Act
|
|
70
|
+
const formatted = createOptions().formatters.log({ err })
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
73
|
+
expect(formatted.error.cause).toBe('ENOTFOUND')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('ecs log formatter falls back to the raw cause value when it has neither message nor code', () => {
|
|
77
|
+
// Arrange
|
|
78
|
+
const err = new Error('fetch failed')
|
|
79
|
+
err.cause = 'ECONNREFUSED'
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
const formatted = createOptions().formatters.log({ err })
|
|
83
|
+
|
|
84
|
+
// Assert
|
|
85
|
+
expect(formatted.error.cause).toBe('ECONNREFUSED')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('ecs log formatter is unaffected when the error has no cause', () => {
|
|
89
|
+
// Arrange
|
|
90
|
+
const err = new Error('plain failure')
|
|
91
|
+
|
|
92
|
+
// Act
|
|
93
|
+
const formatted = createOptions().formatters.log({ err })
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
expect(formatted.error.message).toBe('plain failure')
|
|
97
|
+
expect('cause' in formatted.error).toBe(false)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('non-ecs log formats are unaffected by the cause-preserving wrapper', () => {
|
|
101
|
+
// Arrange
|
|
102
|
+
// Act
|
|
103
|
+
const options = createOptions('json')
|
|
104
|
+
|
|
105
|
+
// Assert
|
|
106
|
+
expect('formatters' in options).toBe(false)
|
|
107
|
+
})
|
|
108
|
+
})
|