@nimbus-cqrs/eventsourcingdb 2.1.2 → 2.3.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/README.md +26 -9
- package/esm/_dnt.polyfills.d.ts +7 -0
- package/esm/_dnt.polyfills.d.ts.map +1 -0
- package/esm/_dnt.polyfills.js +1 -0
- package/esm/index.d.ts +1 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -0
- package/esm/lib/client.d.ts +5 -1
- package/esm/lib/client.d.ts.map +1 -1
- package/esm/lib/client.js +5 -1
- package/esm/lib/eventMapping.d.ts.map +1 -1
- package/esm/lib/eventMapping.js +1 -1
- package/esm/lib/eventObserver.d.ts +54 -6
- package/esm/lib/eventObserver.d.ts.map +1 -1
- package/esm/lib/eventObserver.js +178 -40
- package/esm/lib/tracing.d.ts +40 -0
- package/esm/lib/tracing.d.ts.map +1 -1
- package/esm/lib/tracing.js +74 -0
- package/esm/lib/writeEvents.d.ts.map +1 -1
- package/esm/lib/writeEvents.js +13 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
Integration between Nimbus and [EventSourcingDB](https://www.eventsourcingdb.io/). The package wraps the official `eventsourcingdb` client with:
|
|
9
9
|
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
10
|
+
- a **singleton setup** that pings the server, verifies the API token and registers long-running observers in one call,
|
|
11
|
+
- typed **`writeEvents` / `readEvents`** helpers that translate between Nimbus events and EventSourcingDB events while preserving correlation IDs, data schemas and W3C trace context (`traceparent` / `tracestate`),
|
|
12
|
+
- resilient **event observers** with exponential-backoff retries, jitter, position tracking across reconnects and OpenTelemetry span linking back to the original writer.
|
|
13
13
|
|
|
14
14
|
Refer to the [Nimbus main repository](https://github.com/overlap-dev/Nimbus) or the [Nimbus documentation](https://nimbus.overlap.at) for more information about the Nimbus framework.
|
|
15
15
|
|
|
@@ -122,7 +122,7 @@ await writeEvents(
|
|
|
122
122
|
type: "isSubjectPristine",
|
|
123
123
|
payload: { subject: "/todos/todo-1" },
|
|
124
124
|
},
|
|
125
|
-
]
|
|
125
|
+
],
|
|
126
126
|
);
|
|
127
127
|
```
|
|
128
128
|
|
|
@@ -146,11 +146,17 @@ Pass an `AbortSignal` as the third argument if you need to cancel a long-running
|
|
|
146
146
|
|
|
147
147
|
## Event observers
|
|
148
148
|
|
|
149
|
-
An `EventObserver` is a long-running consumer attached to a subject. `initEventObserver` (or the `eventObservers` array on `setupEventSourcingDBClient`) starts it in the background and keeps it alive
|
|
149
|
+
An `EventObserver` is a long-running consumer attached to a subject. `initEventObserver` (or the `eventObservers` array on `setupEventSourcingDBClient`) starts it in the background and keeps it alive. Connection and handler failures are retried separately with exponential backoff plus jitter (defaults: 3 retries, 3000 ms initial delay for each):
|
|
150
|
+
|
|
151
|
+
- **Connection failures** reconnect the observe stream (`connectionRetryOptions`; deprecated alias: `retryOptions`).
|
|
152
|
+
- **Handler failures** are retried in-place without reconnecting (`handlerRetryOptions`). After handler retries are exhausted the event is skipped (optional `onHandlerError`) and observation continues.
|
|
153
|
+
|
|
154
|
+
On every handled or skipped event the observer advances its lower bound so a reconnection resumes from exactly where it left off — no replays, no gaps.
|
|
150
155
|
|
|
151
156
|
Each event handler runs inside an OpenTelemetry span. If the source event carries a `traceparent`, that span is linked back to the writer's trace, giving you end-to-end visibility from the command that produced the event to every subscriber that reacted to it.
|
|
152
157
|
|
|
153
158
|
```typescript
|
|
159
|
+
import { getLogger } from "@nimbus-cqrs/core";
|
|
154
160
|
import { initEventObserver } from "@nimbus-cqrs/eventsourcingdb";
|
|
155
161
|
|
|
156
162
|
initEventObserver({
|
|
@@ -166,10 +172,21 @@ initEventObserver({
|
|
|
166
172
|
// ...update a read model, send a notification, ...
|
|
167
173
|
}
|
|
168
174
|
},
|
|
169
|
-
|
|
175
|
+
connectionRetryOptions: {
|
|
170
176
|
maxRetries: 5,
|
|
171
177
|
initialRetryDelayMs: 1000,
|
|
172
178
|
},
|
|
179
|
+
handlerRetryOptions: {
|
|
180
|
+
maxRetries: 3,
|
|
181
|
+
initialRetryDelayMs: 1000,
|
|
182
|
+
},
|
|
183
|
+
onHandlerError: (error, event) => {
|
|
184
|
+
getLogger().error({
|
|
185
|
+
category: "EventObserver",
|
|
186
|
+
message: `Skipping event after handler retries: ${event.id}`,
|
|
187
|
+
error,
|
|
188
|
+
});
|
|
189
|
+
},
|
|
173
190
|
});
|
|
174
191
|
```
|
|
175
192
|
|
|
@@ -201,9 +218,9 @@ import {
|
|
|
201
218
|
} from "@nimbus-cqrs/eventsourcingdb";
|
|
202
219
|
```
|
|
203
220
|
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
-
|
|
221
|
+
- `nimbusEventToEventSourcingDBEventCandidate(event)` — manual conversion, e.g. when batching with the raw client.
|
|
222
|
+
- `eventSourcingDBEventToNimbusEvent<TEvent>(event)` — typed conversion when reading or observing.
|
|
223
|
+
- `isEventData(value)` — type guard to detect the Nimbus envelope on arbitrary stored data.
|
|
207
224
|
|
|
208
225
|
# License
|
|
209
226
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dnt.polyfills.d.ts","sourceRoot":"","sources":["../src/_dnt.polyfills.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,KAAK;QACb,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CACF;AAED,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/esm/index.d.ts
CHANGED
package/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
|
package/esm/index.js
CHANGED
package/esm/lib/client.d.ts
CHANGED
|
@@ -59,7 +59,11 @@ export type SetupEventSourcingDBClientInput = {
|
|
|
59
59
|
* eventHandler: async (event: Event) => {
|
|
60
60
|
* console.log('Received event:', event);
|
|
61
61
|
* },
|
|
62
|
-
*
|
|
62
|
+
* connectionRetryOptions: {
|
|
63
|
+
* maxRetries: 3,
|
|
64
|
+
* initialRetryDelayMs: 3000,
|
|
65
|
+
* },
|
|
66
|
+
* handlerRetryOptions: {
|
|
63
67
|
* maxRetries: 3,
|
|
64
68
|
* initialRetryDelayMs: 3000,
|
|
65
69
|
* },
|
package/esm/lib/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,oBAAoB,CAAC;AAI3E;;GAEG;AACH,MAAM,MAAM,+BAA+B,GAAG;IAC1C;;OAEG;IACH,GAAG,EAAE,GAAG,CAAC;IACT;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CACpC,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,oBAAoB,CAAC;AAI3E;;GAEG;AACH,MAAM,MAAM,+BAA+B,GAAG;IAC1C;;OAEG;IACH,GAAG,EAAE,GAAG,CAAC;IACT;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,eAAO,MAAM,0BAA0B,GACnC,mCAAmC,+BAA+B,KACnE,OAAO,CAAC,IAAI,CA0Cd,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,wBAAwB,QAAO,MAQ3C,CAAC"}
|
package/esm/lib/client.js
CHANGED
|
@@ -44,7 +44,11 @@ let eventSourcingDBClient = null;
|
|
|
44
44
|
* eventHandler: async (event: Event) => {
|
|
45
45
|
* console.log('Received event:', event);
|
|
46
46
|
* },
|
|
47
|
-
*
|
|
47
|
+
* connectionRetryOptions: {
|
|
48
|
+
* maxRetries: 3,
|
|
49
|
+
* initialRetryDelayMs: 3000,
|
|
50
|
+
* },
|
|
51
|
+
* handlerRetryOptions: {
|
|
48
52
|
* maxRetries: 3,
|
|
49
53
|
* initialRetryDelayMs: 3000,
|
|
50
54
|
* },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eventMapping.d.ts","sourceRoot":"","sources":["../../src/lib/eventMapping.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"eventMapping.d.ts","sourceRoot":"","sources":["../../src/lib/eventMapping.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,KAAK,EACb,MAAM,qDAAqD,CAAC;AAE7D,OAAO,KAAK,EACR,KAAK,IAAI,oBAAoB,EAC7B,cAAc,EACjB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAAG;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,UAAU,EAAE,mBAAmB,CAAC;CACnC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,OAAO,KAAG,IAAI,IAAI,SAOnD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,0CAA0C,GACnD,OAAO,KAAK,EACZ,cAAc,MAAM,EACpB,aAAa,MAAM,KACpB,cAeF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iCAAiC,GAAI,MAAM,SAAS,KAAK,EAClE,sBAAsB,oBAAoB,KAC3C,MAwBF,CAAC"}
|
package/esm/lib/eventMapping.js
CHANGED
|
@@ -10,8 +10,8 @@ type ObserveFromLatestEvent = {
|
|
|
10
10
|
};
|
|
11
11
|
export type RetryOptions = {
|
|
12
12
|
/**
|
|
13
|
-
* The maximum number of
|
|
14
|
-
* Defaults to 3.
|
|
13
|
+
* The maximum number of retries after the initial attempt
|
|
14
|
+
* before giving up. Defaults to 3 (4 failed attempts total).
|
|
15
15
|
*/
|
|
16
16
|
maxRetries: number;
|
|
17
17
|
/**
|
|
@@ -55,11 +55,33 @@ export type EventObserver = {
|
|
|
55
55
|
*/
|
|
56
56
|
eventHandler: (event: EventSourcingDBEvent) => Promise<void> | void;
|
|
57
57
|
/**
|
|
58
|
-
* Options for retry behavior when the connection fails.
|
|
59
|
-
* Uses exponential backoff with jitter between
|
|
58
|
+
* Options for retry behavior when the observe stream / connection fails.
|
|
59
|
+
* Uses exponential backoff with jitter between reconnects.
|
|
60
60
|
* Defaults to { maxRetries: 3, initialRetryDelayMs: 3000 }.
|
|
61
61
|
*/
|
|
62
|
+
connectionRetryOptions?: RetryOptions;
|
|
63
|
+
/**
|
|
64
|
+
* Options for retry behavior when {@link eventHandler} throws.
|
|
65
|
+
* Handler retries happen in-place without reconnecting the stream.
|
|
66
|
+
* Defaults to { maxRetries: 3, initialRetryDelayMs: 3000 }.
|
|
67
|
+
*/
|
|
68
|
+
handlerRetryOptions?: RetryOptions;
|
|
69
|
+
/**
|
|
70
|
+
* @deprecated Use {@link connectionRetryOptions} instead.
|
|
71
|
+
*
|
|
72
|
+
* Options for retry behavior when the observe stream / connection fails.
|
|
73
|
+
* Ignored when {@link connectionRetryOptions} is set.
|
|
74
|
+
*/
|
|
62
75
|
retryOptions?: RetryOptions;
|
|
76
|
+
/**
|
|
77
|
+
* Called when {@link eventHandler} keeps failing after all handler
|
|
78
|
+
* retries are exhausted. The event is then skipped and observation
|
|
79
|
+
* continues. When omitted, a critical log entry is emitted instead.
|
|
80
|
+
*
|
|
81
|
+
* @param error - The last error thrown by the handler.
|
|
82
|
+
* @param event - The event that could not be handled.
|
|
83
|
+
*/
|
|
84
|
+
onHandlerError?: (error: Error, event: EventSourcingDBEvent) => Promise<void> | void;
|
|
63
85
|
};
|
|
64
86
|
/**
|
|
65
87
|
* Calculates an exponential backoff delay with jitter for a given
|
|
@@ -70,13 +92,39 @@ export type EventObserver = {
|
|
|
70
92
|
* exponential scaling.
|
|
71
93
|
* @param attempt - The zero-based retry attempt number.
|
|
72
94
|
* @returns The backoff delay in milliseconds.
|
|
95
|
+
*
|
|
96
|
+
* @deprecated Import {@link calculateBackoffDelay} from
|
|
97
|
+
* `@nimbus-cqrs/core` instead. This wrapper keeps the historical
|
|
98
|
+
* 30% jitter default used by EventSourcingDB observers.
|
|
73
99
|
*/
|
|
74
100
|
export declare const calculateBackoffDelay: (initialDelayMs: number, attempt: number) => number;
|
|
101
|
+
/**
|
|
102
|
+
* Starts observing events for the given {@link EventObserver} with
|
|
103
|
+
* automatic reconnection on stream failure and separate in-place
|
|
104
|
+
* retries for handler failures.
|
|
105
|
+
*
|
|
106
|
+
* Events are consumed from the stream and each one is passed to the
|
|
107
|
+
* observer's event handler inside an OpenTelemetry span. If the event
|
|
108
|
+
* carries a `traceparent`, the span is linked to the original writer's
|
|
109
|
+
* trace for end-to-end distributed tracing.
|
|
110
|
+
*
|
|
111
|
+
* Handler failures are retried without reconnecting. After handler
|
|
112
|
+
* retries are exhausted the event is skipped (lower bound advanced)
|
|
113
|
+
* so observation can continue. When the connection drops, exponential
|
|
114
|
+
* backoff with jitter is applied up to the configured maximum number
|
|
115
|
+
* of connection retries.
|
|
116
|
+
*
|
|
117
|
+
* @param eventObserver - The event observer configuration.
|
|
118
|
+
* @returns A promise that resolves when the observe stream ends or
|
|
119
|
+
* connection retries are exhausted.
|
|
120
|
+
*/
|
|
121
|
+
export declare const observeWithRetry: (eventObserver: EventObserver) => Promise<void>;
|
|
75
122
|
/**
|
|
76
123
|
* Initializes an event observer by starting the observation loop in
|
|
77
124
|
* the background (non-blocking). The observer will keep running and
|
|
78
|
-
* reconnecting according to its retry options until the
|
|
79
|
-
* or retries are exhausted.
|
|
125
|
+
* reconnecting according to its connection retry options until the
|
|
126
|
+
* stream ends or connection retries are exhausted. Handler failures
|
|
127
|
+
* are retried separately and do not stop the observer.
|
|
80
128
|
*
|
|
81
129
|
* @param eventObserver - The event observer configuration.
|
|
82
130
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eventObserver.d.ts","sourceRoot":"","sources":["../../src/lib/eventObserver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"eventObserver.d.ts","sourceRoot":"","sources":["../../src/lib/eventObserver.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,KAAK,IAAI,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAarE,KAAK,KAAK,GAAG;IACT,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,GAAG,WAAW,CAAC;CACnC,CAAC;AAEF,KAAK,sBAAsB,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;CAC1D,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACvB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,mBAAmB,EAAE,MAAM,CAAC;CAC/B,CAAC;AAOF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IACxB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB;;;OAGG;IACH,eAAe,CAAC,EAAE,sBAAsB,CAAC;IACzC;;;;;OAKG;IACH,YAAY,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,YAAY,CAAC;IACtC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,YAAY,CAAC;IACnC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,CACb,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,oBAAoB,KAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC7B,CAAC;AAQF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GAC9B,gBAAgB,MAAM,EACtB,SAAS,MAAM,KAChB,MAIG,CAAC;AAgPP;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,gBAAgB,GACzB,eAAe,aAAa,KAC7B,OAAO,CAAC,IAAI,CAiGd,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAAI,eAAe,aAAa,KAAG,IAEhE,CAAC"}
|
package/esm/lib/eventObserver.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { getLogger } from '@nimbus-cqrs/core';
|
|
1
|
+
import { calculateBackoffDelay as coreCalculateBackoffDelay, getLogger, withRetry, } from '@nimbus-cqrs/core';
|
|
2
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
2
3
|
import { getEventSourcingDBClient } from './client.js';
|
|
3
|
-
import {
|
|
4
|
+
import { isEventData } from './eventMapping.js';
|
|
5
|
+
import { observerConnectionReconnectsCounter, observerConnectionRetryAttemptsCounter, observerEventsHandledCounter, observerHandlerRetryAttemptsCounter, observerHandlingDuration, withObserveEventSpan, } from './tracing.js';
|
|
6
|
+
const DEFAULT_RETRY_OPTIONS = {
|
|
7
|
+
maxRetries: 3,
|
|
8
|
+
initialRetryDelayMs: 3000,
|
|
9
|
+
};
|
|
4
10
|
/**
|
|
5
11
|
* Returns a promise that resolves after the given number of milliseconds.
|
|
6
12
|
*/
|
|
@@ -14,13 +20,37 @@ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
14
20
|
* exponential scaling.
|
|
15
21
|
* @param attempt - The zero-based retry attempt number.
|
|
16
22
|
* @returns The backoff delay in milliseconds.
|
|
23
|
+
*
|
|
24
|
+
* @deprecated Import {@link calculateBackoffDelay} from
|
|
25
|
+
* `@nimbus-cqrs/core` instead. This wrapper keeps the historical
|
|
26
|
+
* 30% jitter default used by EventSourcingDB observers.
|
|
17
27
|
*/
|
|
18
|
-
export const calculateBackoffDelay = (initialDelayMs, attempt) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
export const calculateBackoffDelay = (initialDelayMs, attempt) => coreCalculateBackoffDelay(initialDelayMs, attempt, {
|
|
29
|
+
jitterFactor: 0.3,
|
|
30
|
+
maxDelayMs: Infinity,
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Resolves connection retry options, preferring
|
|
34
|
+
* {@link EventObserver.connectionRetryOptions} over the deprecated
|
|
35
|
+
* {@link EventObserver.retryOptions}.
|
|
36
|
+
*/
|
|
37
|
+
const resolveConnectionRetryOptions = (eventObserver) => ({
|
|
38
|
+
maxRetries: eventObserver.connectionRetryOptions?.maxRetries ??
|
|
39
|
+
eventObserver.retryOptions?.maxRetries ??
|
|
40
|
+
DEFAULT_RETRY_OPTIONS.maxRetries,
|
|
41
|
+
initialRetryDelayMs: eventObserver.connectionRetryOptions?.initialRetryDelayMs ??
|
|
42
|
+
eventObserver.retryOptions?.initialRetryDelayMs ??
|
|
43
|
+
DEFAULT_RETRY_OPTIONS.initialRetryDelayMs,
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Resolves handler retry options from {@link EventObserver.handlerRetryOptions}.
|
|
47
|
+
*/
|
|
48
|
+
const resolveHandlerRetryOptions = (eventObserver) => ({
|
|
49
|
+
maxRetries: eventObserver.handlerRetryOptions?.maxRetries ??
|
|
50
|
+
DEFAULT_RETRY_OPTIONS.maxRetries,
|
|
51
|
+
initialRetryDelayMs: eventObserver.handlerRetryOptions?.initialRetryDelayMs ??
|
|
52
|
+
DEFAULT_RETRY_OPTIONS.initialRetryDelayMs,
|
|
53
|
+
});
|
|
24
54
|
/**
|
|
25
55
|
* Logs an informational message when an event observer connects or
|
|
26
56
|
* reconnects to EventSourcingDB. When {@link retryCount} is greater
|
|
@@ -39,10 +69,10 @@ const logObserverConnection = (subject, retryCount, data) => {
|
|
|
39
69
|
getLogger().info({ category: 'Nimbus', message, data });
|
|
40
70
|
};
|
|
41
71
|
/**
|
|
42
|
-
* Handles
|
|
43
|
-
* backoff before the next
|
|
44
|
-
* retries is exceeded a critical log entry is
|
|
45
|
-
* retries are attempted.
|
|
72
|
+
* Handles a connection / stream error by logging it and waiting with
|
|
73
|
+
* exponential backoff before the next reconnect attempt. When the
|
|
74
|
+
* maximum number of retries is exceeded a critical log entry is
|
|
75
|
+
* emitted and no further retries are attempted.
|
|
46
76
|
*
|
|
47
77
|
* @param error - The error that caused the observer to disconnect.
|
|
48
78
|
* @param subject - The observed subject.
|
|
@@ -53,7 +83,7 @@ const logObserverConnection = (subject, retryCount, data) => {
|
|
|
53
83
|
* @returns `true` if the observer should retry, `false` if retries
|
|
54
84
|
* are exhausted.
|
|
55
85
|
*/
|
|
56
|
-
const
|
|
86
|
+
const handleConnectionError = async (error, subject, retryCount, maxRetries, initialRetryDelayMs) => {
|
|
57
87
|
if (retryCount > maxRetries) {
|
|
58
88
|
getLogger().critical({
|
|
59
89
|
category: 'Nimbus',
|
|
@@ -61,6 +91,7 @@ const handleObserverError = async (error, subject, retryCount, maxRetries, initi
|
|
|
61
91
|
});
|
|
62
92
|
return false;
|
|
63
93
|
}
|
|
94
|
+
observerConnectionRetryAttemptsCounter.add(1, { subject });
|
|
64
95
|
const backoffDelay = calculateBackoffDelay(initialRetryDelayMs, retryCount - 1);
|
|
65
96
|
getLogger().error({
|
|
66
97
|
category: 'Nimbus',
|
|
@@ -70,32 +101,130 @@ const handleObserverError = async (error, subject, retryCount, maxRetries, initi
|
|
|
70
101
|
await delay(backoffDelay);
|
|
71
102
|
return true;
|
|
72
103
|
};
|
|
104
|
+
const recordHandlerMetrics = (metricLabels, status, startTime) => {
|
|
105
|
+
observerEventsHandledCounter.add(1, { ...metricLabels, status });
|
|
106
|
+
observerHandlingDuration.record((performance.now() - startTime) / 1000, metricLabels);
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Invokes the event handler with in-place retries. Does not reconnect
|
|
110
|
+
* the observe stream. When all retries are exhausted, calls
|
|
111
|
+
* {@link EventObserver.onHandlerError} or logs critically, then
|
|
112
|
+
* returns without throwing so the observer can skip the event.
|
|
113
|
+
*/
|
|
114
|
+
const handleEventWithRetry = async (eventObserver, event, handlerRetryOptions, span, startTime) => {
|
|
115
|
+
const { maxRetries, initialRetryDelayMs } = handlerRetryOptions;
|
|
116
|
+
const metricLabels = {
|
|
117
|
+
subject: eventObserver.subject,
|
|
118
|
+
event_type: event.type,
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
await withRetry(() => eventObserver.eventHandler(event), {
|
|
122
|
+
maxRetries,
|
|
123
|
+
initialDelayMs: initialRetryDelayMs,
|
|
124
|
+
maxDelayMs: Infinity,
|
|
125
|
+
jitterFactor: 0.3,
|
|
126
|
+
onRetry: ({ error, attempt, delayMs }) => {
|
|
127
|
+
observerHandlerRetryAttemptsCounter.add(1, metricLabels);
|
|
128
|
+
span.addEvent('retry', {
|
|
129
|
+
attempt,
|
|
130
|
+
delay_ms: delayMs,
|
|
131
|
+
});
|
|
132
|
+
getLogger().error({
|
|
133
|
+
category: 'Nimbus',
|
|
134
|
+
message: `Error handling event "${event.id}" (${event.type}) for subject "${eventObserver.subject}" (retry ${attempt}/${maxRetries}), retrying in ${delayMs}ms`,
|
|
135
|
+
error,
|
|
136
|
+
...(isEventData(event.data)
|
|
137
|
+
? {
|
|
138
|
+
correlationId: event.data.nimbusMeta.correlationid,
|
|
139
|
+
}
|
|
140
|
+
: {}),
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
recordHandlerMetrics(metricLabels, 'success', startTime);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const handlerError = error instanceof Error
|
|
148
|
+
? error
|
|
149
|
+
: new Error(String(error));
|
|
150
|
+
recordHandlerMetrics(metricLabels, 'skipped', startTime);
|
|
151
|
+
span.setStatus({
|
|
152
|
+
code: SpanStatusCode.ERROR,
|
|
153
|
+
message: handlerError.message,
|
|
154
|
+
});
|
|
155
|
+
span.recordException(handlerError);
|
|
156
|
+
await handleHandlerExhausted(eventObserver, event, handlerError, maxRetries);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* Handles exhausted handler retries by invoking
|
|
161
|
+
* {@link EventObserver.onHandlerError} or emitting a critical log.
|
|
162
|
+
*/
|
|
163
|
+
const handleHandlerExhausted = async (eventObserver, event, handlerError, maxRetries) => {
|
|
164
|
+
if (eventObserver.onHandlerError) {
|
|
165
|
+
try {
|
|
166
|
+
await eventObserver.onHandlerError(handlerError, event);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
// Isolate user callback failures so the poison
|
|
170
|
+
// event is still skipped and observation continues.
|
|
171
|
+
const callbackError = err instanceof Error
|
|
172
|
+
? err
|
|
173
|
+
: new Error(String(err));
|
|
174
|
+
getLogger().critical({
|
|
175
|
+
category: 'Nimbus',
|
|
176
|
+
message: `onHandlerError failed for event "${event.id}" (${event.type}) for subject "${eventObserver.subject}". Skipping event.`,
|
|
177
|
+
error: callbackError,
|
|
178
|
+
...(isEventData(event.data)
|
|
179
|
+
? {
|
|
180
|
+
correlationId: event.data.nimbusMeta.correlationid,
|
|
181
|
+
}
|
|
182
|
+
: {}),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
getLogger().critical({
|
|
188
|
+
category: 'Nimbus',
|
|
189
|
+
message: `Failed to handle event "${event.id}" (${event.type}) for subject "${eventObserver.subject}" after ${maxRetries} ${maxRetries === 1 ? 'retry' : 'retries'}. Skipping event.`,
|
|
190
|
+
error: handlerError,
|
|
191
|
+
...(isEventData(event.data)
|
|
192
|
+
? {
|
|
193
|
+
correlationId: event.data.nimbusMeta.correlationid,
|
|
194
|
+
}
|
|
195
|
+
: {}),
|
|
196
|
+
});
|
|
197
|
+
};
|
|
73
198
|
/**
|
|
74
199
|
* Starts observing events for the given {@link EventObserver} with
|
|
75
|
-
* automatic reconnection on failure
|
|
76
|
-
*
|
|
77
|
-
* On each connection attempt the EventSourcingDB server is pinged
|
|
78
|
-
* first. Events are then consumed from the stream and each one is
|
|
79
|
-
* passed to the observer's event handler inside an OpenTelemetry
|
|
80
|
-
* span. If the event carries a `traceparent`, the span is linked to
|
|
81
|
-
* the original writer's trace for end-to-end distributed tracing.
|
|
200
|
+
* automatic reconnection on stream failure and separate in-place
|
|
201
|
+
* retries for handler failures.
|
|
82
202
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
203
|
+
* Events are consumed from the stream and each one is passed to the
|
|
204
|
+
* observer's event handler inside an OpenTelemetry span. If the event
|
|
205
|
+
* carries a `traceparent`, the span is linked to the original writer's
|
|
206
|
+
* trace for end-to-end distributed tracing.
|
|
85
207
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
208
|
+
* Handler failures are retried without reconnecting. After handler
|
|
209
|
+
* retries are exhausted the event is skipped (lower bound advanced)
|
|
210
|
+
* so observation can continue. When the connection drops, exponential
|
|
211
|
+
* backoff with jitter is applied up to the configured maximum number
|
|
212
|
+
* of connection retries.
|
|
88
213
|
*
|
|
89
214
|
* @param eventObserver - The event observer configuration.
|
|
215
|
+
* @returns A promise that resolves when the observe stream ends or
|
|
216
|
+
* connection retries are exhausted.
|
|
90
217
|
*/
|
|
91
|
-
const observeWithRetry = async (eventObserver) => {
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
let retryCount = 0;
|
|
218
|
+
export const observeWithRetry = async (eventObserver) => {
|
|
219
|
+
const connectionRetryOptions = resolveConnectionRetryOptions(eventObserver);
|
|
220
|
+
const handlerRetryOptions = resolveHandlerRetryOptions(eventObserver);
|
|
221
|
+
let connectionRetryCount = 0;
|
|
96
222
|
let lastProcessedEventId;
|
|
97
223
|
while (true) {
|
|
98
224
|
try {
|
|
225
|
+
// Resolve the client on every attempt so a re-initialized
|
|
226
|
+
// singleton is picked up after reconnecting to EventSourcingDB.
|
|
227
|
+
const eventSourcingDBClient = getEventSourcingDBClient();
|
|
99
228
|
// Once we have a concrete position, use it as lower bound and
|
|
100
229
|
// drop fromLatestEvent; otherwise fall back to the original options.
|
|
101
230
|
const lowerBound = lastProcessedEventId
|
|
@@ -104,7 +233,7 @@ const observeWithRetry = async (eventObserver) => {
|
|
|
104
233
|
const fromLatestEvent = lastProcessedEventId
|
|
105
234
|
? undefined
|
|
106
235
|
: eventObserver.fromLatestEvent;
|
|
107
|
-
logObserverConnection(eventObserver.subject,
|
|
236
|
+
logObserverConnection(eventObserver.subject, connectionRetryCount, {
|
|
108
237
|
recursive: eventObserver.recursive ?? false,
|
|
109
238
|
lowerBound,
|
|
110
239
|
fromLatestEvent,
|
|
@@ -114,26 +243,34 @@ const observeWithRetry = async (eventObserver) => {
|
|
|
114
243
|
...(lowerBound ? { lowerBound } : {}),
|
|
115
244
|
...(fromLatestEvent ? { fromLatestEvent } : {}),
|
|
116
245
|
})) {
|
|
117
|
-
//
|
|
118
|
-
|
|
246
|
+
// Stream is healthy once events flow again. Count a successful
|
|
247
|
+
// reconnect only here so failed reconnect attempts are excluded.
|
|
248
|
+
if (connectionRetryCount > 0) {
|
|
249
|
+
observerConnectionReconnectsCounter.add(1, {
|
|
250
|
+
subject: eventObserver.subject,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
connectionRetryCount = 0;
|
|
119
254
|
const traceContext = event.traceparent
|
|
120
255
|
? {
|
|
121
256
|
traceparent: event.traceparent,
|
|
122
257
|
tracestate: event.tracestate,
|
|
123
258
|
}
|
|
124
259
|
: undefined;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
260
|
+
const startTime = performance.now();
|
|
261
|
+
await withObserveEventSpan(event, eventObserver.subject, traceContext, async (span) => {
|
|
262
|
+
await handleEventWithRetry(eventObserver, event, handlerRetryOptions, span, startTime);
|
|
263
|
+
});
|
|
264
|
+
// Advance after success or skip so poison events do not block
|
|
265
|
+
// the stream and reconnects resume past the last attempted event.
|
|
129
266
|
lastProcessedEventId = event.id;
|
|
130
267
|
}
|
|
131
268
|
// If the loop completes normally (stream ended), we're done
|
|
132
269
|
return;
|
|
133
270
|
}
|
|
134
271
|
catch (error) {
|
|
135
|
-
|
|
136
|
-
const shouldRetry = await
|
|
272
|
+
connectionRetryCount++;
|
|
273
|
+
const shouldRetry = await handleConnectionError(error, eventObserver.subject, connectionRetryCount, connectionRetryOptions.maxRetries, connectionRetryOptions.initialRetryDelayMs);
|
|
137
274
|
if (!shouldRetry) {
|
|
138
275
|
return;
|
|
139
276
|
}
|
|
@@ -143,8 +280,9 @@ const observeWithRetry = async (eventObserver) => {
|
|
|
143
280
|
/**
|
|
144
281
|
* Initializes an event observer by starting the observation loop in
|
|
145
282
|
* the background (non-blocking). The observer will keep running and
|
|
146
|
-
* reconnecting according to its retry options until the
|
|
147
|
-
* or retries are exhausted.
|
|
283
|
+
* reconnecting according to its connection retry options until the
|
|
284
|
+
* stream ends or connection retries are exhausted. Handler failures
|
|
285
|
+
* are retried separately and do not stop the observer.
|
|
148
286
|
*
|
|
149
287
|
* @param eventObserver - The event observer configuration.
|
|
150
288
|
*/
|
package/esm/lib/tracing.d.ts
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
|
+
import { type Span } from '@opentelemetry/api';
|
|
1
2
|
export declare const tracer: import("@opentelemetry/api").Tracer;
|
|
2
3
|
export declare const DB_SYSTEM = "eventsourcingdb";
|
|
4
|
+
/**
|
|
5
|
+
* Histogram of serialized Nimbus event sizes written via writeEvents.
|
|
6
|
+
*/
|
|
7
|
+
export declare const eventSizeBytes: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
|
|
8
|
+
/**
|
|
9
|
+
* Counter of events handled by an event observer, labeled with
|
|
10
|
+
* `status: success|skipped`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const observerEventsHandledCounter: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
13
|
+
/**
|
|
14
|
+
* Histogram of observer handler duration in seconds (includes retries).
|
|
15
|
+
*/
|
|
16
|
+
export declare const observerHandlingDuration: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
|
|
17
|
+
/**
|
|
18
|
+
* Counter of in-place handler retry attempts for event observers.
|
|
19
|
+
*/
|
|
20
|
+
export declare const observerHandlerRetryAttemptsCounter: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
21
|
+
/**
|
|
22
|
+
* Counter of connection reconnect attempts scheduled after stream failure.
|
|
23
|
+
*/
|
|
24
|
+
export declare const observerConnectionRetryAttemptsCounter: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
25
|
+
/**
|
|
26
|
+
* Counter of successful reconnects after one or more connection retries.
|
|
27
|
+
*/
|
|
28
|
+
export declare const observerConnectionReconnectsCounter: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
3
29
|
/**
|
|
4
30
|
* Trace context extracted from an EventSourcingDB event, used to link
|
|
5
31
|
* the processing span to the span that originally wrote the event.
|
|
@@ -22,6 +48,20 @@ export type TraceContext = {
|
|
|
22
48
|
* @returns The result of the async function
|
|
23
49
|
*/
|
|
24
50
|
export declare const withSpan: <T>(operation: string, fn: () => Promise<T>, traceContext?: TraceContext) => Promise<T>;
|
|
51
|
+
/**
|
|
52
|
+
* Starts a consumer span for observing a single EventSourcingDB event,
|
|
53
|
+
* optionally linked to the writer's trace via {@link TraceContext}.
|
|
54
|
+
*
|
|
55
|
+
* @param event - The observed EventSourcingDB event.
|
|
56
|
+
* @param observerSubject - The observer's configured subject.
|
|
57
|
+
* @param traceContext - Optional writer trace context from the event.
|
|
58
|
+
* @param fn - Work to run inside the active span.
|
|
59
|
+
*/
|
|
60
|
+
export declare const withObserveEventSpan: <T>(event: {
|
|
61
|
+
id: string;
|
|
62
|
+
type: string;
|
|
63
|
+
subject: string;
|
|
64
|
+
}, observerSubject: string, traceContext: TraceContext | undefined, fn: (span: Span) => Promise<T>) => Promise<T>;
|
|
25
65
|
/**
|
|
26
66
|
* Wraps an async generator with OpenTelemetry tracing and metrics.
|
|
27
67
|
*
|
package/esm/lib/tracing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../../src/lib/tracing.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../../src/lib/tracing.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,KAAK,IAAI,EAIZ,MAAM,oBAAoB,CAAC;AAE5B,eAAO,MAAM,MAAM,qCAA4B,CAAC;AAEhD,eAAO,MAAM,SAAS,oBAAoB,CAAC;AAmB3C;;GAEG;AACH,eAAO,MAAM,cAAc,iFAM1B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,+EAMxC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,wBAAwB,iFAOpC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mCAAmC,+EAM/C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sCAAsC,+EAMlD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mCAAmC,+EAM/C,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,EACtB,WAAW,MAAM,EACjB,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,eAAe,YAAY,KAC5B,OAAO,CAAC,CAAC,CA6DX,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAClC,OAAO;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EACpD,iBAAiB,MAAM,EACvB,cAAc,YAAY,GAAG,SAAS,EACtC,IAAI,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAC/B,OAAO,CAAC,CAAC,CA8BX,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAuB,sBAAsB,CAAC,CAAC,EAC3C,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GACxC,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CA0C/B"}
|
package/esm/lib/tracing.js
CHANGED
|
@@ -9,6 +9,45 @@ const operationDuration = meter.createHistogram('eventsourcingdb_operation_durat
|
|
|
9
9
|
description: 'Duration of EventSourcingDB operations in seconds',
|
|
10
10
|
unit: 's',
|
|
11
11
|
});
|
|
12
|
+
/**
|
|
13
|
+
* Histogram of serialized Nimbus event sizes written via writeEvents.
|
|
14
|
+
*/
|
|
15
|
+
export const eventSizeBytes = meter.createHistogram('eventsourcingdb_event_size_bytes', {
|
|
16
|
+
description: 'Size of events written to EventSourcingDB in bytes',
|
|
17
|
+
unit: 'By',
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Counter of events handled by an event observer, labeled with
|
|
21
|
+
* `status: success|skipped`.
|
|
22
|
+
*/
|
|
23
|
+
export const observerEventsHandledCounter = meter.createCounter('eventsourcingdb_observer_events_handled_total', {
|
|
24
|
+
description: 'Total number of events handled by EventSourcingDB observers',
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Histogram of observer handler duration in seconds (includes retries).
|
|
28
|
+
*/
|
|
29
|
+
export const observerHandlingDuration = meter.createHistogram('eventsourcingdb_observer_handling_duration_seconds', {
|
|
30
|
+
description: 'Duration of EventSourcingDB observer event handling in seconds',
|
|
31
|
+
unit: 's',
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Counter of in-place handler retry attempts for event observers.
|
|
35
|
+
*/
|
|
36
|
+
export const observerHandlerRetryAttemptsCounter = meter.createCounter('eventsourcingdb_observer_handler_retry_attempts_total', {
|
|
37
|
+
description: 'Total number of handler retry attempts for EventSourcingDB observers',
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Counter of connection reconnect attempts scheduled after stream failure.
|
|
41
|
+
*/
|
|
42
|
+
export const observerConnectionRetryAttemptsCounter = meter.createCounter('eventsourcingdb_observer_connection_retry_attempts_total', {
|
|
43
|
+
description: 'Total number of connection retry attempts for EventSourcingDB observers',
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Counter of successful reconnects after one or more connection retries.
|
|
47
|
+
*/
|
|
48
|
+
export const observerConnectionReconnectsCounter = meter.createCounter('eventsourcingdb_observer_connection_reconnects_total', {
|
|
49
|
+
description: 'Total number of successful connection reconnects for EventSourcingDB observers',
|
|
50
|
+
});
|
|
12
51
|
/**
|
|
13
52
|
* Wraps an async function with OpenTelemetry tracing and metrics.
|
|
14
53
|
*
|
|
@@ -68,6 +107,41 @@ export const withSpan = (operation, fn, traceContext) => {
|
|
|
68
107
|
}
|
|
69
108
|
});
|
|
70
109
|
};
|
|
110
|
+
/**
|
|
111
|
+
* Starts a consumer span for observing a single EventSourcingDB event,
|
|
112
|
+
* optionally linked to the writer's trace via {@link TraceContext}.
|
|
113
|
+
*
|
|
114
|
+
* @param event - The observed EventSourcingDB event.
|
|
115
|
+
* @param observerSubject - The observer's configured subject.
|
|
116
|
+
* @param traceContext - Optional writer trace context from the event.
|
|
117
|
+
* @param fn - Work to run inside the active span.
|
|
118
|
+
*/
|
|
119
|
+
export const withObserveEventSpan = (event, observerSubject, traceContext, fn) => {
|
|
120
|
+
const parentContext = traceContext
|
|
121
|
+
? propagation.extract(otelContext.active(), traceContext)
|
|
122
|
+
: otelContext.active();
|
|
123
|
+
return tracer.startActiveSpan('eventsourcingdb.observeEvent', {
|
|
124
|
+
kind: SpanKind.CONSUMER,
|
|
125
|
+
attributes: {
|
|
126
|
+
'db.system': DB_SYSTEM,
|
|
127
|
+
'db.operation': 'observeEvent',
|
|
128
|
+
'messaging.system': 'eventsourcingdb',
|
|
129
|
+
'messaging.operation': 'process',
|
|
130
|
+
'messaging.destination': event.type,
|
|
131
|
+
'cloudevents.event_id': event.id,
|
|
132
|
+
'cloudevents.event_type': event.type,
|
|
133
|
+
'cloudevents.event_subject': event.subject,
|
|
134
|
+
'eventsourcingdb.observer.subject': observerSubject,
|
|
135
|
+
},
|
|
136
|
+
}, parentContext, async (span) => {
|
|
137
|
+
try {
|
|
138
|
+
return await fn(span);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
span.end();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
};
|
|
71
145
|
/**
|
|
72
146
|
* Wraps an async generator with OpenTelemetry tracing and metrics.
|
|
73
147
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeEvents.d.ts","sourceRoot":"","sources":["../../src/lib/writeEvents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qDAAqD,CAAC;AAEjF,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"writeEvents.d.ts","sourceRoot":"","sources":["../../src/lib/writeEvents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qDAAqD,CAAC;AAEjF,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAapE;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GACpB,QAAQ,KAAK,EAAE,EACf,gBAAgB,YAAY,EAAE,KAC/B,OAAO,CAAC,IAAI,CAyBd,CAAC"}
|
package/esm/lib/writeEvents.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { context, propagation } from '@opentelemetry/api';
|
|
2
2
|
import { getEventSourcingDBClient } from './client.js';
|
|
3
3
|
import { nimbusEventToEventSourcingDBEventCandidate } from './eventMapping.js';
|
|
4
|
-
import { withSpan } from './tracing.js';
|
|
4
|
+
import { eventSizeBytes, withSpan } from './tracing.js';
|
|
5
|
+
const textEncoder = new TextEncoder();
|
|
6
|
+
/**
|
|
7
|
+
* Returns the UTF-8 byte length of a serialized Nimbus event.
|
|
8
|
+
*/
|
|
9
|
+
const getEventSizeBytes = (event) => textEncoder.encode(JSON.stringify(event)).length;
|
|
5
10
|
/**
|
|
6
11
|
* Writes one or more Nimbus events to EventSourcingDB. Each event is
|
|
7
12
|
* converted into an EventSourcingDB event candidate before being persisted.
|
|
@@ -14,7 +19,13 @@ export const writeEvents = (events, preconditions) => {
|
|
|
14
19
|
const eventSourcingDBClient = getEventSourcingDBClient();
|
|
15
20
|
const carrier = {};
|
|
16
21
|
propagation.inject(context.active(), carrier);
|
|
17
|
-
const eventCandidates = events.map((event) =>
|
|
22
|
+
const eventCandidates = events.map((event) => {
|
|
23
|
+
eventSizeBytes.record(getEventSizeBytes(event), {
|
|
24
|
+
subject: event.subject,
|
|
25
|
+
event_type: event.type,
|
|
26
|
+
});
|
|
27
|
+
return nimbusEventToEventSourcingDBEventCandidate(event, carrier['traceparent'], carrier['tracestate']);
|
|
28
|
+
});
|
|
18
29
|
await eventSourcingDBClient.writeEvents(eventCandidates, preconditions);
|
|
19
30
|
});
|
|
20
31
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimbus-cqrs/eventsourcingdb",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Simplify Event-Driven Applications - EventSourcingDB integration for the Nimbus framework.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nimbus",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@opentelemetry/api": "^1.9.1",
|
|
38
|
-
"eventsourcingdb": "^1.8.
|
|
39
|
-
"zod": "^4.3
|
|
40
|
-
"@nimbus-cqrs/core": "^2.
|
|
38
|
+
"eventsourcingdb": "^1.8.2",
|
|
39
|
+
"zod": "^4.4.3",
|
|
40
|
+
"@nimbus-cqrs/core": "^2.3.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^22.0.0"
|