@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.1.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 { getTraceId } from '@defra/hapi-tracing'
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
- ...ecsFormat({
21
- serviceVersion,
22
- serviceName
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 traceId = getTraceId()
69
+ const correlationId = requestContext.get('correlation_id')
46
70
 
47
- if (traceId) {
48
- mixinValues.trace = { id: traceId }
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
+ })