@toa.io/core 1.0.0-alpha.243 → 1.0.0-alpha.246

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": "@toa.io/core",
3
- "version": "1.0.0-alpha.243",
3
+ "version": "1.0.0-alpha.246",
4
4
  "description": "Toa Core",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -22,10 +22,10 @@
22
22
  "dependencies": {
23
23
  "@rsql/parser": "1.2.4",
24
24
  "@toa.io/generic": "1.0.0-alpha.225",
25
- "@toa.io/yaml": "1.0.0-alpha.225",
25
+ "@toa.io/yaml": "1.0.0-alpha.246",
26
26
  "error-value": "0.3.0",
27
- "openspan": "1.0.0-alpha.173",
27
+ "openspan": "1.0.0-alpha.246",
28
28
  "uuid": "11.1.1"
29
29
  },
30
- "gitHead": "743e8060d35ca750f70487c04aef9698591c5edd"
30
+ "gitHead": "d6ee2a7246c64a44c50c28fadcfd1e27aaf137cb"
31
31
  }
package/src/call.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { Readable } = require('node:stream')
4
+ const { current, encode } = require('openspan')
4
5
  const { Connector } = require('./connector')
5
6
  const { Err } = require('error-value')
6
7
 
@@ -25,6 +26,11 @@ class Call extends Connector {
25
26
  // avoid validation on the recipient's side
26
27
  request.authentic = true
27
28
 
29
+ const context = current()
30
+
31
+ if (context !== undefined)
32
+ request.telemetry = encode(context)
33
+
28
34
  const reply = await this.#transmitter.request(request)
29
35
 
30
36
  if (reply === null) return null
package/src/component.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const assert = require('node:assert')
4
- const { console } = require('openspan')
4
+ const { console, current, decode, run } = require('openspan')
5
5
  const { Connector } = require('./connector')
6
6
 
7
7
  class Component extends Connector {
@@ -10,6 +10,9 @@ class Component extends Connector {
10
10
  /** @protected */
11
11
  operations
12
12
 
13
+ /** @protected */
14
+ kind = 'server'
15
+
13
16
  constructor (locator, operations) {
14
17
  super()
15
18
 
@@ -23,15 +26,39 @@ class Component extends Connector {
23
26
  assert.ok(endpoint in this.operations,
24
27
  `Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
25
28
 
26
- const reply = await this.operations[endpoint].invoke(request)
29
+ // if the request carries no telemetry, the trace starts here
30
+ const remote = request?.telemetry === undefined ? null : decode(request.telemetry)
31
+ const task = () => this.process(endpoint, request)
32
+
33
+ if (remote === null)
34
+ return task()
35
+ else
36
+ return run(remote, task)
37
+ }
38
+
39
+ /** @private */
40
+ async process (endpoint, request) {
41
+ const options = { name: `${this.locator.id}.${endpoint}`, kind: this.kind }
42
+
43
+ // the server span is emitted by the component itself,
44
+ // while the client span belongs to the calling service and inherits it from the context
45
+ if (this.kind === 'server')
46
+ options.service = this.locator.id
47
+
48
+ return console.span(options, async () => {
49
+ const reply = await this.operations[endpoint].invoke(request)
50
+
51
+ if (reply?.exception !== undefined) {
52
+ current().status = 'error'
27
53
 
28
- if (reply?.exception !== undefined)
29
- console.error('Failed to execute operation', {
30
- endpoint: `${this.locator.id}.${endpoint}`,
31
- exception: reply.exception
32
- })
54
+ console.error('Failed to execute operation', {
55
+ endpoint: `${this.locator.id}.${endpoint}`,
56
+ exception: reply.exception
57
+ })
58
+ }
33
59
 
34
- return reply
60
+ return reply
61
+ })
35
62
  }
36
63
  }
37
64
 
package/src/connector.js CHANGED
@@ -85,10 +85,18 @@ class Connector {
85
85
 
86
86
  this.#disconnecting = undefined
87
87
 
88
- this.#connecting = (async () => {
88
+ const work = async () => {
89
89
  await Promise.all(this.#dependencies.map((connector) => connector.connect()))
90
90
  await this.open()
91
- })()
91
+ }
92
+
93
+ // anonymous grouping nodes are not worth a span
94
+ this.#connecting = TRACE_BOOT && this.constructor.name !== 'Connector'
95
+ ? console.span({
96
+ name: `connect ${this.constructor.name}`,
97
+ attributes: { id: this.locator?.id ?? this.id }
98
+ }, work)
99
+ : work()
92
100
 
93
101
  try {
94
102
  await this.#connecting
@@ -177,5 +185,6 @@ class Connector {
177
185
  }
178
186
 
179
187
  const DELAY = 5000
188
+ const TRACE_BOOT = process.env.TOA_BOOT_TRACE === '1'
180
189
 
181
190
  exports.Connector = Connector
package/src/event.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { console, current, encode } = require('openspan')
3
4
  const { Connector } = require('./connector')
4
5
 
5
6
  /**
@@ -12,11 +13,15 @@ class Event extends Connector {
12
13
  #conditioned
13
14
  #subjective
14
15
 
16
+ /** @type {string} */
17
+ #label
18
+
15
19
  constructor (definition, emitter, bridge = undefined) {
16
20
  super()
17
21
 
18
22
  this.#conditioned = definition.conditioned
19
23
  this.#subjective = definition.subjective
24
+ this.#label = definition.label ?? 'event'
20
25
  this.#emitter = emitter
21
26
  this.#bridge = bridge
22
27
 
@@ -32,7 +37,20 @@ class Event extends Connector {
32
37
  /** @type {toa.core.Message} */
33
38
  const message = { payload }
34
39
 
35
- await this.#emitter.emit(message)
40
+ const options = {
41
+ name: `${this.#label} publish`,
42
+ kind: 'producer',
43
+ attributes: { 'messaging.destination.name': this.#label }
44
+ }
45
+
46
+ await console.span(options, async () => {
47
+ const context = current()
48
+
49
+ if (context !== undefined)
50
+ message.telemetry = encode(context)
51
+
52
+ await this.#emitter.emit(message)
53
+ })
36
54
  }
37
55
  }
38
56
  }
package/src/receiver.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('openspan')
3
+ const { console, decode, run } = require('openspan')
4
4
  const { add } = require('@toa.io/generic')
5
5
  const { Connector } = require('./connector')
6
6
 
@@ -17,6 +17,12 @@ class Receiver extends Connector {
17
17
  /** @type {string} */
18
18
  #endpoint
19
19
 
20
+ /** @type {string} */
21
+ #label
22
+
23
+ /** @type {string} */
24
+ #destination
25
+
20
26
  /** @type {unknown[]} */
21
27
  #arguments
22
28
 
@@ -32,6 +38,8 @@ class Receiver extends Connector {
32
38
  this.#conditioned = conditioned
33
39
  this.#adaptive = adaptive
34
40
  this.#endpoint = operation
41
+ this.#label = definition.label ?? operation
42
+ this.#destination = definition.destination ?? this.#label
35
43
  this.#arguments = definition.arguments
36
44
 
37
45
  this.#local = local
@@ -43,7 +51,7 @@ class Receiver extends Connector {
43
51
 
44
52
  /** @hot */
45
53
  async receive (message) {
46
- const { payload, ...extensions } = message
54
+ const { payload, telemetry, ...extensions } = message
47
55
 
48
56
  if (this.#conditioned && await this.#bridge.condition(payload) === false) return
49
57
 
@@ -51,17 +59,51 @@ class Receiver extends Connector {
51
59
 
52
60
  add(request, extensions)
53
61
 
54
- try {
55
- await this.#local.invoke(this.#endpoint, request)
56
- } catch (error) {
57
- console.error('Receiver error', {
58
- component: this.#local.locator.id,
59
- endpoint: this.#endpoint,
60
- error
61
- })
62
+ // continue the trace from the producer span
63
+ const remote = telemetry === undefined ? null : decode(telemetry)
64
+ const task = () => this.#process(request)
62
65
 
63
- throw error
66
+ if (remote === null)
67
+ await task()
68
+ else
69
+ await run(remote, task)
70
+ }
71
+
72
+ async #process (request) {
73
+ /*
74
+ * The delivery span is created on behalf of the messaging destination, so that
75
+ * each consumer forms its own complete producer/consumer pair, and service graphs
76
+ * display fan-out correctly: producer -> destination -> each consumer
77
+ * (Tempo pairs spans one-to-one, thus multiple consumers can not pair
78
+ * with a single producer span, see grafana/tempo#5408)
79
+ */
80
+ const delivery = {
81
+ name: `${this.#label} deliver`,
82
+ kind: 'producer',
83
+ service: this.#destination,
84
+ attributes: { 'messaging.destination.name': this.#destination }
85
+ }
86
+
87
+ const options = {
88
+ name: `${this.#label} process`,
89
+ kind: 'consumer',
90
+ service: this.#local.locator.id,
91
+ attributes: { 'messaging.destination.name': this.#destination }
64
92
  }
93
+
94
+ return console.span(delivery, async () => console.span(options, async () => {
95
+ try {
96
+ await this.#local.invoke(this.#endpoint, request)
97
+ } catch (error) {
98
+ console.error('Receiver error', {
99
+ component: this.#local.locator.id,
100
+ endpoint: this.#endpoint,
101
+ error
102
+ })
103
+
104
+ throw error
105
+ }
106
+ }))
65
107
  }
66
108
 
67
109
  async #request (payload) {
package/src/remote.js CHANGED
@@ -4,6 +4,8 @@ const assert = require('node:assert')
4
4
  const { Component } = require('./component')
5
5
 
6
6
  class Remote extends Component {
7
+ kind = 'client'
8
+
7
9
  explain (endpoint) {
8
10
  assert.ok(endpoint in this.operations,
9
11
  `Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
@@ -41,7 +41,7 @@ describe('condition', () => {
41
41
  await emit()
42
42
 
43
43
  const payload = await fixtures.bridge.payload.mock.results[0].value
44
- const message = { payload }
44
+ const message = { payload, telemetry: expect.any(String) }
45
45
  expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
46
46
  })
47
47
 
@@ -71,7 +71,7 @@ describe('condition', () => {
71
71
  expect(fixtures.bridge.condition).not.toHaveBeenCalledWith()
72
72
 
73
73
  const payload = await fixtures.bridge.payload.mock.results[0].value
74
- const message = { payload }
74
+ const message = { payload, telemetry: expect.any(String) }
75
75
 
76
76
  expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
77
77
  })
@@ -84,7 +84,7 @@ describe('payload', () => {
84
84
  await emit()
85
85
 
86
86
  const payload = await fixtures.bridge.payload.mock.results[0].value
87
- const message = { payload }
87
+ const message = { payload, telemetry: expect.any(String) }
88
88
 
89
89
  expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
90
90
  })
@@ -110,7 +110,7 @@ describe('payload', () => {
110
110
  await emit()
111
111
 
112
112
  const payload = fixtures.event.state
113
- const message = { payload }
113
+ const message = { payload, telemetry: expect.any(String) }
114
114
 
115
115
  expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
116
116
  })
@@ -9,6 +9,7 @@ const definition = /** @type {toa.norm.component.Receiver} */ {
9
9
  }
10
10
 
11
11
  const local = /** @type {toa.core.Component} */ {
12
+ locator: { id: 'default.dummy' },
12
13
  invoke: jest.fn()
13
14
  }
14
15
 
@@ -1,3 +1,4 @@
1
1
  export interface Message<T = any> {
2
2
  payload: T
3
+ telemetry?: string // W3C traceparent
3
4
  }
@@ -18,6 +18,7 @@ export interface Request {
18
18
  query?: Query
19
19
  authentic?: boolean
20
20
  task?: boolean
21
+ telemetry?: string // W3C traceparent
21
22
  }
22
23
 
23
24
  export interface Reply {