@cratis/chronicle 6.32.1 → 6.34.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.
Files changed (189) hide show
  1. package/ChronicleClient.ts +35 -33
  2. package/ChronicleOptions.ts +27 -1
  3. package/EventStore.ts +14 -12
  4. package/Metrics.ts +70 -96
  5. package/Tracing.ts +4 -2
  6. package/WellKnownTelemetryNames.ts +67 -0
  7. package/artifacts/validateArtifactSchemas.spec.ts +1 -1
  8. package/artifacts/withActivatedArtifact.ts +7 -5
  9. package/connection/ChronicleConnection.ts +18 -6
  10. package/connection/CompatibilityPreflight.ts +2 -1
  11. package/connection/KernelKeepAlive.ts +13 -7
  12. package/connection/TokenProvider.ts +3 -1
  13. package/connection/TraceContextMiddleware.ts +30 -0
  14. package/connection/for_ChronicleConnection/when_creating_clients/with_correlated_multi_host_callers.spec.ts +73 -0
  15. package/connection/for_OAuthTokenProvider/when_acquiring_tokens/with_http_instrumentation.spec.ts +102 -0
  16. package/connection/for_TraceContextMiddleware/when_calling/with_active_context.spec.ts +235 -0
  17. package/connection/for_TraceContextMiddleware/when_calling/with_grpc_instrumentation.spec.ts +102 -0
  18. package/correlation/CorrelationIdManager.ts +10 -0
  19. package/dist/ChronicleClient.d.ts.map +1 -1
  20. package/dist/ChronicleClient.js +35 -33
  21. package/dist/ChronicleClient.js.map +1 -1
  22. package/dist/ChronicleOptions.d.ts +8 -0
  23. package/dist/ChronicleOptions.d.ts.map +1 -1
  24. package/dist/ChronicleOptions.js +19 -1
  25. package/dist/ChronicleOptions.js.map +1 -1
  26. package/dist/EventStore.d.ts +3 -1
  27. package/dist/EventStore.d.ts.map +1 -1
  28. package/dist/EventStore.js +14 -12
  29. package/dist/EventStore.js.map +1 -1
  30. package/dist/Metrics.d.ts +23 -64
  31. package/dist/Metrics.d.ts.map +1 -1
  32. package/dist/Metrics.js +62 -92
  33. package/dist/Metrics.js.map +1 -1
  34. package/dist/Tracing.d.ts +1 -1
  35. package/dist/Tracing.d.ts.map +1 -1
  36. package/dist/Tracing.js +4 -2
  37. package/dist/Tracing.js.map +1 -1
  38. package/dist/WellKnownTelemetryNames.d.ts +65 -0
  39. package/dist/WellKnownTelemetryNames.d.ts.map +1 -0
  40. package/dist/WellKnownTelemetryNames.js +67 -0
  41. package/dist/WellKnownTelemetryNames.js.map +1 -0
  42. package/dist/artifacts/withActivatedArtifact.d.ts +2 -1
  43. package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -1
  44. package/dist/artifacts/withActivatedArtifact.js +5 -4
  45. package/dist/artifacts/withActivatedArtifact.js.map +1 -1
  46. package/dist/connection/ChronicleConnection.d.ts +5 -0
  47. package/dist/connection/ChronicleConnection.d.ts.map +1 -1
  48. package/dist/connection/ChronicleConnection.js +13 -6
  49. package/dist/connection/ChronicleConnection.js.map +1 -1
  50. package/dist/connection/CompatibilityPreflight.d.ts.map +1 -1
  51. package/dist/connection/CompatibilityPreflight.js +2 -1
  52. package/dist/connection/CompatibilityPreflight.js.map +1 -1
  53. package/dist/connection/KernelKeepAlive.d.ts +3 -1
  54. package/dist/connection/KernelKeepAlive.d.ts.map +1 -1
  55. package/dist/connection/KernelKeepAlive.js +10 -7
  56. package/dist/connection/KernelKeepAlive.js.map +1 -1
  57. package/dist/connection/TokenProvider.d.ts.map +1 -1
  58. package/dist/connection/TokenProvider.js +3 -1
  59. package/dist/connection/TokenProvider.js.map +1 -1
  60. package/dist/connection/TraceContextMiddleware.d.ts +4 -0
  61. package/dist/connection/TraceContextMiddleware.d.ts.map +1 -0
  62. package/dist/connection/TraceContextMiddleware.js +30 -0
  63. package/dist/connection/TraceContextMiddleware.js.map +1 -0
  64. package/dist/correlation/CorrelationIdManager.d.ts +4 -0
  65. package/dist/correlation/CorrelationIdManager.d.ts.map +1 -1
  66. package/dist/correlation/CorrelationIdManager.js +8 -0
  67. package/dist/correlation/CorrelationIdManager.js.map +1 -1
  68. package/dist/eventSequences/EventLog.d.ts +2 -1
  69. package/dist/eventSequences/EventLog.d.ts.map +1 -1
  70. package/dist/eventSequences/EventLog.js +2 -2
  71. package/dist/eventSequences/EventLog.js.map +1 -1
  72. package/dist/eventSequences/EventSequence.d.ts +4 -1
  73. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  74. package/dist/eventSequences/EventSequence.js +63 -80
  75. package/dist/eventSequences/EventSequence.js.map +1 -1
  76. package/dist/eventStoreSubscriptions/EventStoreSubscriptions.d.ts.map +1 -1
  77. package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js +3 -4
  78. package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js.map +1 -1
  79. package/dist/index.d.ts +7 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +4 -1
  82. package/dist/index.js.map +1 -1
  83. package/dist/logging/ChronicleLogEntry.d.ts +11 -0
  84. package/dist/logging/ChronicleLogEntry.d.ts.map +1 -0
  85. package/dist/logging/ChronicleLogEntry.js +4 -0
  86. package/dist/logging/ChronicleLogEntry.js.map +1 -0
  87. package/dist/logging/ChronicleLogLevel.d.ts +9 -0
  88. package/dist/logging/ChronicleLogLevel.d.ts.map +1 -0
  89. package/dist/logging/ChronicleLogLevel.js +12 -0
  90. package/dist/logging/ChronicleLogLevel.js.map +1 -0
  91. package/dist/logging/DiagChronicleLogger.d.ts +7 -0
  92. package/dist/logging/DiagChronicleLogger.d.ts.map +1 -0
  93. package/dist/logging/DiagChronicleLogger.js +10 -0
  94. package/dist/logging/DiagChronicleLogger.js.map +1 -0
  95. package/dist/logging/IChronicleLogger.d.ts +7 -0
  96. package/dist/logging/IChronicleLogger.d.ts.map +1 -0
  97. package/dist/logging/IChronicleLogger.js +4 -0
  98. package/dist/logging/IChronicleLogger.js.map +1 -0
  99. package/dist/logging/createLogger.d.ts +10 -0
  100. package/dist/logging/createLogger.d.ts.map +1 -0
  101. package/dist/logging/createLogger.js +55 -0
  102. package/dist/logging/createLogger.js.map +1 -0
  103. package/dist/projections/Projections.d.ts.map +1 -1
  104. package/dist/projections/Projections.js +4 -3
  105. package/dist/projections/Projections.js.map +1 -1
  106. package/dist/reactors/Reactors.d.ts.map +1 -1
  107. package/dist/reactors/Reactors.js +26 -24
  108. package/dist/reactors/Reactors.js.map +1 -1
  109. package/dist/readModels/IReadModels.d.ts +14 -0
  110. package/dist/readModels/IReadModels.d.ts.map +1 -1
  111. package/dist/readModels/ReadModels.d.ts +6 -0
  112. package/dist/readModels/ReadModels.d.ts.map +1 -1
  113. package/dist/readModels/ReadModels.js +110 -0
  114. package/dist/readModels/ReadModels.js.map +1 -1
  115. package/dist/readModels/for_ReadModels/given/a_stored_read_model.d.ts +20 -0
  116. package/dist/readModels/for_ReadModels/given/a_stored_read_model.d.ts.map +1 -0
  117. package/dist/readModels/for_ReadModels/given/a_stored_read_model.js +79 -0
  118. package/dist/readModels/for_ReadModels/given/a_stored_read_model.js.map +1 -0
  119. package/dist/reducers/Reducers.d.ts.map +1 -1
  120. package/dist/reducers/Reducers.js +28 -25
  121. package/dist/reducers/Reducers.js.map +1 -1
  122. package/dist/seeding/EventSeeding.d.ts.map +1 -1
  123. package/dist/seeding/EventSeeding.js +4 -3
  124. package/dist/seeding/EventSeeding.js.map +1 -1
  125. package/dist/telemetry/ChronicleTelemetryOptions.d.ts +11 -0
  126. package/dist/telemetry/ChronicleTelemetryOptions.d.ts.map +1 -0
  127. package/dist/telemetry/ChronicleTelemetryOptions.js +4 -0
  128. package/dist/telemetry/ChronicleTelemetryOptions.js.map +1 -0
  129. package/dist/telemetry/observeOperation.d.ts +6 -0
  130. package/dist/telemetry/observeOperation.d.ts.map +1 -0
  131. package/dist/telemetry/observeOperation.js +23 -0
  132. package/dist/telemetry/observeOperation.js.map +1 -0
  133. package/dist/telemetry/runInBackgroundContext.d.ts +3 -0
  134. package/dist/telemetry/runInBackgroundContext.d.ts.map +1 -0
  135. package/dist/telemetry/runInBackgroundContext.js +9 -0
  136. package/dist/telemetry/runInBackgroundContext.js.map +1 -0
  137. package/dist/telemetry/spanAttributes.d.ts +14 -0
  138. package/dist/telemetry/spanAttributes.d.ts.map +1 -0
  139. package/dist/telemetry/spanAttributes.js +47 -0
  140. package/dist/telemetry/spanAttributes.js.map +1 -0
  141. package/dist/tsconfig.tsbuildinfo +1 -1
  142. package/eventSequences/EventLog.ts +4 -2
  143. package/eventSequences/EventSequence.ts +64 -81
  144. package/eventStoreSubscriptions/EventStoreSubscriptions.ts +5 -5
  145. package/for_ChronicleClient/when_starting_background_work/with_a_correlated_caller.spec.ts +150 -0
  146. package/for_ChronicleOptions/when_configuring_telemetry/with_event_source_identifiers.spec.ts +44 -0
  147. package/index.ts +7 -1
  148. package/logging/ChronicleLogEntry.ts +14 -0
  149. package/logging/ChronicleLogLevel.ts +11 -0
  150. package/logging/DiagChronicleLogger.ts +13 -0
  151. package/logging/IChronicleLogger.ts +10 -0
  152. package/logging/createLogger.ts +50 -0
  153. package/logging/for_ChronicleLogger/when_logging/with_client_owned_sinks.spec.ts +96 -0
  154. package/package.json +7 -1
  155. package/projections/Projections.ts +4 -3
  156. package/reactors/Reactors.ts +27 -25
  157. package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +14 -14
  158. package/readModels/IReadModels.ts +15 -0
  159. package/readModels/ReadModels.ts +118 -1
  160. package/readModels/for_ReadModels/given/a_stored_read_model.ts +62 -0
  161. package/readModels/for_ReadModels/when_releasing_a_document/with_a_default_subject.spec.ts +39 -0
  162. package/readModels/for_ReadModels/when_releasing_a_document/with_a_frozen_document.spec.ts +28 -0
  163. package/readModels/for_ReadModels/when_releasing_a_document/with_a_malformed_response.spec.ts +24 -0
  164. package/readModels/for_ReadModels/when_releasing_a_document/with_a_reported_failure.spec.ts +38 -0
  165. package/readModels/for_ReadModels/when_releasing_a_document/with_an_empty_released_value.spec.ts +19 -0
  166. package/readModels/for_ReadModels/when_releasing_a_document/with_an_unknown_subject_property.spec.ts +42 -0
  167. package/readModels/for_ReadModels/when_releasing_a_document/with_bookkeeping_fields.spec.ts +27 -0
  168. package/readModels/for_ReadModels/when_releasing_a_document/with_conflicting_or_missing_properties.spec.ts +32 -0
  169. package/readModels/for_ReadModels/when_releasing_a_document/with_empty_subject_metadata.spec.ts +56 -0
  170. package/readModels/for_ReadModels/when_releasing_a_document/with_invalid_subject_metadata.spec.ts +27 -0
  171. package/readModels/for_ReadModels/when_releasing_a_document/with_mixed_subjects.spec.ts +38 -0
  172. package/readModels/for_ReadModels/when_releasing_a_document/with_property_subjects.spec.ts +52 -0
  173. package/readModels/for_ReadModels/when_releasing_a_document/with_whitespace_subjects.spec.ts +24 -0
  174. package/readModels/for_ReadModels/when_releasing_a_document/without_subjects.spec.ts +24 -0
  175. package/reducers/Reducers.ts +29 -26
  176. package/seeding/EventSeeding.ts +6 -4
  177. package/telemetry/ChronicleTelemetryOptions.ts +8 -0
  178. package/telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.ts +36 -0
  179. package/telemetry/for_ChronicleTelemetry/given/an_event_sequence.fixture.ts +29 -0
  180. package/telemetry/for_ChronicleTelemetry/when_appending/with_shared_and_legacy_attributes.spec.ts +58 -0
  181. package/telemetry/for_ChronicleTelemetry/when_appending/without_an_sdk.spec.ts +15 -0
  182. package/telemetry/for_ChronicleTelemetry/when_documenting/with_public_names.spec.ts +15 -0
  183. package/telemetry/for_ChronicleTelemetry/when_recording_failures/with_sensitive_exception_details.spec.ts +34 -0
  184. package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts +92 -0
  185. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_legacy_overlap.spec.ts +40 -0
  186. package/telemetry/for_ChronicleTelemetry/when_recording_sequence_numbers/with_integer_precision.spec.ts +23 -0
  187. package/telemetry/observeOperation.ts +25 -0
  188. package/telemetry/runInBackgroundContext.ts +10 -0
  189. package/telemetry/spanAttributes.ts +49 -0
@@ -0,0 +1,235 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { afterEach, beforeEach, chai, describe, it, vi } from 'vitest';
5
+ import { context, propagation, trace } from '@opentelemetry/api';
6
+ import { createServer, ServerError, Status, type ServiceImplementation } from 'nice-grpc';
7
+ import { Metadata } from 'nice-grpc-common';
8
+ import { ConnectionServiceDefinition, EventSequencesDefinition, EventStoresDefinition, ReactorsDefinition } from '@cratis/chronicle.contracts';
9
+ import { ChronicleConnection } from '../../ChronicleConnection.js';
10
+ import { OAuthTokenProvider } from '../../TokenProvider.js';
11
+ import { CorrelationId, correlationIdManager } from '../../../correlation/index.js';
12
+ import { telemetrySession } from '../../../telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.js';
13
+
14
+ const should = chai.should();
15
+ const telemetry = telemetrySession();
16
+ const correlation = 'f5a978af-cd69-4a98-9f1e-7f656cc8ba2a';
17
+ let server: ReturnType<typeof createServer>;
18
+ let connection: ChronicleConnection;
19
+ let received: Metadata[];
20
+ let original: Metadata;
21
+ let traceparent: string;
22
+ let address: string;
23
+ let rejectNext: boolean;
24
+
25
+ beforeEach(async () => {
26
+ received = [];
27
+ rejectNext = false;
28
+ server = createServer().use(async function* (call, callContext) {
29
+ received.push(Metadata(callContext.metadata));
30
+ return yield* call.next(call.request, callContext);
31
+ });
32
+ const unused = async () => { throw new Error('Unexpected RPC'); };
33
+ server.add(ConnectionServiceDefinition, {
34
+ ...Object.fromEntries(Object.keys(ConnectionServiceDefinition.methods).map(name => [name, unused])),
35
+ checkCompatibility: async () => ({ IsCompatible: true, Incompatibilities: [], ServerVersion: 'test' }),
36
+ connectionKeepAlive: async () => ({})
37
+ } as ServiceImplementation<typeof ConnectionServiceDefinition>);
38
+ server.add(EventStoresDefinition, {
39
+ allEventStores: async () => {
40
+ if (rejectNext) {
41
+ rejectNext = false;
42
+ throw new ServerError(Status.UNAUTHENTICATED, 'Refresh required');
43
+ }
44
+ return { IsAuthorized: true, Data: [] };
45
+ },
46
+ ensureEventStore: unused,
47
+ observeEventStores: async function* () { yield { IsAuthorized: true, Data: [] }; }
48
+ } as ServiceImplementation<typeof EventStoresDefinition>);
49
+ server.add(EventSequencesDefinition, {
50
+ ...Object.fromEntries(Object.keys(EventSequencesDefinition.methods).map(name => [name, unused])),
51
+ append: async () => ({ Response: { SequenceNumber: 0n } })
52
+ } as ServiceImplementation<typeof EventSequencesDefinition>);
53
+ server.add(ReactorsDefinition, {
54
+ ...Object.fromEntries(Object.keys(ReactorsDefinition.methods).map(name => [name, unused])),
55
+ observe: async function* (requests) {
56
+ for await (const _request of requests) {
57
+ yield { Events: [], Partition: '', ReplayState: 0 };
58
+ return;
59
+ }
60
+ }
61
+ } as ServiceImplementation<typeof ReactorsDefinition>);
62
+ const port = await server.listen('127.0.0.1:0');
63
+ address = `chronicle://127.0.0.1:${port}?disableTls=true`;
64
+ connection = new ChronicleConnection({ connectionString: `${address}&apiKey=api-secret` });
65
+ original = Metadata({ authorization: 'Bearer caller-token', 'x-custom': 'preserved',
66
+ 'custom-bin': new Uint8Array([1, 2, 3]), baggage: 'email=private', traceparent: 'stale' });
67
+ });
68
+
69
+ afterEach(async () => {
70
+ connection?.dispose();
71
+ await server?.shutdown();
72
+ vi.restoreAllMocks();
73
+ });
74
+
75
+ async function inContext(action: (parent: string) => Promise<void>, correlationValue = correlation) {
76
+ const span = telemetry.traces.getTracer('host').startSpan('host');
77
+ const spanContext = span.spanContext();
78
+ const parent = `00-${spanContext.traceId}-${spanContext.spanId}-01`;
79
+ traceparent = parent;
80
+ const active = propagation.setBaggage(trace.setSpan(context.active(), span), propagation.createBaggage({
81
+ 'cratis.correlation_id': { value: correlationValue }, email: { value: 'private@example.com' },
82
+ 'cratis.event_source.id': { value: 'private-source' }, 'cratis.tenant.id': { value: 'private-tenant' }
83
+ }));
84
+ try {
85
+ await context.with(active, async () => {
86
+ await Promise.resolve();
87
+ await action(parent);
88
+ propagation.getBaggage(context.active())!.getEntry('email')!.value.should.equal('private@example.com');
89
+ });
90
+ } finally { span.end(); }
91
+ }
92
+
93
+ describe('when calling unary RPCs with active trace context and sensitive baggage', () => {
94
+ beforeEach(async () => {
95
+ await inContext(async () => {
96
+ await connection.connect();
97
+ await connection.eventStores.allEventStores({}, { metadata: original });
98
+ await connection.connections.connectionKeepAlive({ ConnectionId: 'test' });
99
+ await connection.reconnect();
100
+ await connection.eventStores.allEventStores({}, { metadata: original });
101
+ });
102
+ });
103
+ it('should detach connect and reconnect compatibility checks from caller context', () => {
104
+ received.should.have.lengthOf(5);
105
+ for (const index of [0, 3]) {
106
+ should.equal(received[index].get('traceparent'), undefined);
107
+ should.equal(received[index].get('baggage'), undefined);
108
+ should.equal(received[index].get('api-key'), 'api-secret');
109
+ }
110
+ });
111
+ it('should inject trace context and only correlation baggage on business calls and keep-alive', () => {
112
+ for (const index of [1, 2, 4]) {
113
+ should.equal(received[index].get('traceparent'), traceparent);
114
+ should.equal(received[index].get('baggage'), `cratis.correlation_id=${correlation}`);
115
+ should.equal(received[index].get('api-key'), 'api-secret');
116
+ }
117
+ });
118
+ it('should preserve authentication and unrelated string and binary metadata', () => {
119
+ const metadata = received[1];
120
+ should.equal(metadata.get('authorization'), 'Bearer caller-token');
121
+ should.equal(metadata.get('x-custom'), 'preserved');
122
+ Array.from(metadata.get('custom-bin')!).should.deep.equal([1, 2, 3]);
123
+ });
124
+ it('should not mutate the caller carrier', () => {
125
+ should.equal(original.get('traceparent'), 'stale');
126
+ should.equal(original.get('baggage'), 'email=private');
127
+ should.equal(original.get('api-key'), undefined);
128
+ });
129
+ });
130
+
131
+ describe('when calling an event sequence without a cached compatibility verdict', () => {
132
+ beforeEach(async () => {
133
+ await connection.resetChannel();
134
+ await correlationIdManager.run(new CorrelationId(correlation), () => inContext(async () => {
135
+ await connection.eventSequences.append({}, { metadata: original });
136
+ correlationIdManager.scoped!.toString().should.equal(correlation);
137
+ }));
138
+ });
139
+ it('should detach the shared compatibility check while preserving the business caller context', () => {
140
+ received.should.have.lengthOf(2);
141
+ should.equal(received[0].get('traceparent'), undefined);
142
+ should.equal(received[0].get('baggage'), undefined);
143
+ should.equal(received[1].get('traceparent'), traceparent);
144
+ should.equal(received[1].get('baggage'), `cratis.correlation_id=${correlation}`);
145
+ should.equal(received[1].get('authorization'), 'Bearer caller-token');
146
+ });
147
+ });
148
+
149
+ describe('when opening server and bidirectional streams', () => {
150
+ beforeEach(async () => {
151
+ await connection.connect();
152
+ received.length = 0;
153
+ await inContext(async () => {
154
+ for await (const _response of connection.eventStores.observeEventStores({}, { metadata: original })) { break; }
155
+ async function* requests() { yield { Content: undefined }; }
156
+ for await (const _response of connection.reactors.observe(requests(), { metadata: original })) { break; }
157
+ });
158
+ });
159
+ it('should propagate the stream-opening context and preserve authentication', () => {
160
+ received.should.have.lengthOf(2);
161
+ for (const metadata of received) {
162
+ should.equal(metadata.get('traceparent'), traceparent);
163
+ should.equal(metadata.get('baggage'), `cratis.correlation_id=${correlation}`);
164
+ should.equal(metadata.get('authorization'), 'Bearer caller-token');
165
+ }
166
+ });
167
+ });
168
+
169
+ describe('when authentication refresh retries a unary call', () => {
170
+ beforeEach(async () => {
171
+ connection.dispose();
172
+ vi.spyOn(OAuthTokenProvider.prototype, 'getAccessToken').mockResolvedValue('old-token');
173
+ vi.spyOn(OAuthTokenProvider.prototype, 'refresh').mockResolvedValue('refreshed-token');
174
+ connection = new ChronicleConnection({ connectionString: address });
175
+ await connection.connect();
176
+ received.length = 0;
177
+ rejectNext = true;
178
+ await inContext(async () => {
179
+ await connection.eventStores.allEventStores({}, { metadata: original });
180
+ });
181
+ });
182
+ it('should keep trace context and filtered baggage on both attempts with the refreshed authorization', () => {
183
+ received.should.have.lengthOf(2);
184
+ for (const metadata of received) {
185
+ should.equal(metadata.get('traceparent'), traceparent);
186
+ should.equal(metadata.get('baggage'), `cratis.correlation_id=${correlation}`);
187
+ }
188
+ should.equal(received[0].get('authorization'), 'Bearer old-token');
189
+ should.equal(received[1].get('authorization'), 'Bearer refreshed-token');
190
+ });
191
+ });
192
+
193
+ describe('when callers in two contexts use the same connection concurrently', () => {
194
+ const expected = new Map<string, { parent: string; correlationId: string }>();
195
+ beforeEach(async () => {
196
+ await connection.connect();
197
+ received.length = 0;
198
+ expected.clear();
199
+ const correlations = [correlation, '7144abf3-94bd-4429-93e5-3faf23f0bad1'];
200
+ let ready = 0;
201
+ let release!: () => void;
202
+ const bothReady = new Promise<void>(resolve => { release = resolve; });
203
+ await Promise.all(correlations.map((correlationId, index) =>
204
+ correlationIdManager.run(new CorrelationId(correlationId), () => inContext(async parent => {
205
+ const id = index.toString();
206
+ expected.set(id, { parent, correlationId });
207
+ if (++ready === 2) release();
208
+ await bothReady;
209
+ await connection.eventStores.allEventStores({}, { metadata: Metadata({ 'x-call': id }) });
210
+ correlationIdManager.scoped!.toString().should.equal(correlationId);
211
+ }, correlationId))));
212
+ });
213
+ it('should propagate each caller trace and business correlation without cross-contamination', () => {
214
+ received.should.have.lengthOf(2);
215
+ for (const metadata of received) {
216
+ const caller = expected.get(metadata.get('x-call')!)!;
217
+ should.equal(metadata.get('traceparent'), caller.parent);
218
+ should.equal(metadata.get('baggage'), `cratis.correlation_id=${caller.correlationId}`);
219
+ }
220
+ new Set(received.map(metadata => metadata.get('traceparent'))).size.should.equal(2);
221
+ });
222
+ });
223
+
224
+ describe('when calling without an active span or baggage', () => {
225
+ beforeEach(async () => {
226
+ await connection.connect();
227
+ received.length = 0;
228
+ await connection.eventStores.allEventStores({}, { metadata: original });
229
+ });
230
+ it('should remove stale propagation without losing other metadata', () => {
231
+ should.equal(received[0].get('traceparent'), undefined);
232
+ should.equal(received[0].get('baggage'), undefined);
233
+ should.equal(received[0].get('authorization'), 'Bearer caller-token');
234
+ });
235
+ });
@@ -0,0 +1,102 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { afterAll, beforeAll, beforeEach, chai, describe, it } from 'vitest';
5
+ import { context, propagation, trace } from '@opentelemetry/api';
6
+ import { GrpcInstrumentation } from '@opentelemetry/instrumentation-grpc';
7
+ import type { ServiceImplementation } from 'nice-grpc';
8
+ import { Metadata } from 'nice-grpc-common';
9
+ import type { ChronicleConnection } from '../../ChronicleConnection.js';
10
+ import { telemetrySession } from '../../../telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.js';
11
+
12
+ const should = chai.should();
13
+ const telemetry = telemetrySession();
14
+ const instrumentation = new GrpcInstrumentation({ enabled: false });
15
+ const correlation = 'f5a978af-cd69-4a98-9f1e-7f656cc8ba2a';
16
+ let server: ReturnType<typeof import('nice-grpc').createServer>;
17
+ let connection: ChronicleConnection;
18
+ const received: Metadata[] = [];
19
+ let hostTraceId: string;
20
+
21
+ beforeAll(async () => {
22
+ instrumentation.setTracerProvider(telemetry.traces);
23
+ instrumentation.enable();
24
+ // Load grpc-js only after the real instrumentation's require hooks are installed.
25
+ const { createServer } = await import('nice-grpc');
26
+ const { ConnectionServiceDefinition, EventStoresDefinition, ReactorsDefinition } = await import('@cratis/chronicle.contracts');
27
+ const { ChronicleConnection } = await import('../../ChronicleConnection.js');
28
+ server = createServer().use(async function* (call, callContext) {
29
+ received.push(Metadata(callContext.metadata));
30
+ return yield* call.next(call.request, callContext);
31
+ });
32
+ const unused = async () => { throw new Error('Unexpected RPC'); };
33
+ server.add(ConnectionServiceDefinition, {
34
+ ...Object.fromEntries(Object.keys(ConnectionServiceDefinition.methods).map(name => [name, unused])),
35
+ checkCompatibility: async () => ({ IsCompatible: true, Incompatibilities: [], ServerVersion: 'test' })
36
+ } as ServiceImplementation<typeof ConnectionServiceDefinition>);
37
+ server.add(EventStoresDefinition, {
38
+ allEventStores: async () => ({ IsAuthorized: true, Data: [] }),
39
+ ensureEventStore: unused,
40
+ observeEventStores: async function* () {
41
+ yield { IsAuthorized: true, Data: [] };
42
+ yield { IsAuthorized: true, Data: [] };
43
+ }
44
+ } as ServiceImplementation<typeof EventStoresDefinition>);
45
+ server.add(ReactorsDefinition, {
46
+ ...Object.fromEntries(Object.keys(ReactorsDefinition.methods).map(name => [name, unused])),
47
+ observe: async function* (requests) {
48
+ for await (const _request of requests) {
49
+ yield { Events: [], Partition: '', ReplayState: 0 };
50
+ }
51
+ }
52
+ } as ServiceImplementation<typeof ReactorsDefinition>);
53
+ const port = await server.listen('127.0.0.1:0');
54
+ connection = new ChronicleConnection({ connectionString: `chronicle://127.0.0.1:${port}?disableTls=true&apiKey=test` });
55
+ await connection.connect();
56
+ });
57
+
58
+ afterAll(async () => {
59
+ connection?.dispose();
60
+ await server?.shutdown();
61
+ instrumentation.disable();
62
+ });
63
+
64
+ describe('when gRPC auto-instrumentation reinjects active context for unary and streaming calls', () => {
65
+ beforeEach(async () => {
66
+ received.length = 0;
67
+ const span = telemetry.traces.getTracer('host').startSpan('host');
68
+ hostTraceId = span.spanContext().traceId;
69
+ const active = propagation.setBaggage(trace.setSpan(context.active(), span), propagation.createBaggage({
70
+ 'cratis.correlation_id': { value: correlation },
71
+ email: { value: 'private@example.com' },
72
+ 'cratis.event_source.id': { value: 'private-source' },
73
+ 'cratis.tenant.id': { value: 'private-tenant' }
74
+ }));
75
+ try {
76
+ await context.with(active, async () => {
77
+ await connection.eventStores.allEventStores({});
78
+ for await (const _response of connection.eventStores.observeEventStores({})) {
79
+ context.active().should.equal(active);
80
+ await Promise.resolve();
81
+ }
82
+ async function* requests() { yield { Content: undefined }; yield { Content: undefined }; }
83
+ for await (const _response of connection.reactors.observe(requests())) {
84
+ context.active().should.equal(active);
85
+ await Promise.resolve();
86
+ }
87
+ propagation.getBaggage(context.active())!.getEntry('email')!.value.should.equal('private@example.com');
88
+ });
89
+ } finally { span.end(); }
90
+ });
91
+ it('should never let auto-instrumentation restore forbidden baggage on the wire', () => {
92
+ received.should.have.lengthOf(3);
93
+ for (const metadata of received) {
94
+ should.equal(metadata.get('baggage'), `cratis.correlation_id=${correlation}`);
95
+ metadata.get('traceparent')!.should.contain(hostTraceId);
96
+ }
97
+ });
98
+ it('should actually execute the enabled gRPC instrumentation', () => {
99
+ telemetry.spans.getFinishedSpans().filter(span => span.instrumentationScope.name === '@opentelemetry/instrumentation-grpc')
100
+ .length.should.be.greaterThan(0);
101
+ });
102
+ });
@@ -18,11 +18,21 @@ export class CorrelationIdManager implements ICorrelationIdAccessor, ICorrelatio
18
18
  return this._storage.getStore() ?? CorrelationId.create();
19
19
  }
20
20
 
21
+ /** The explicitly scoped identifier, without generating an identifier when no scope exists. */
22
+ get scoped(): CorrelationId | undefined {
23
+ return this._storage.getStore();
24
+ }
25
+
21
26
  /** Runs an async or synchronous operation with a correlation id, restoring the caller afterward. */
22
27
  run<T>(correlationId: CorrelationId, callback: () => T): T {
23
28
  return this._storage.run(correlationId, callback);
24
29
  }
25
30
 
31
+ /** Runs work without inheriting a business correlation, restoring the caller afterward. */
32
+ runWithout<T>(callback: () => T): T {
33
+ return this._storage.exit(callback);
34
+ }
35
+
26
36
  /** @inheritdoc */
27
37
  setCurrent(correlationId: CorrelationId): void {
28
38
  this._storage.enterWith(correlationId);
@@ -1 +1 @@
1
- {"version":3,"file":"ChronicleClient.d.ts","sourceRoot":"","sources":["../ChronicleClient.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAUzD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAO/C;;;;;;;;;;;;GAYG;AACH,qBAAa,eAAgB,YAAW,gBAAgB;IA0BxC,QAAQ,CAAC,OAAO,EAAE,gBAAgB;IAzB9C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,sBAAsB,CAAQ;IACtD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAU;IAE/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsB;IAClD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IAExD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAErB;IAEH,OAAO,CAAC,eAAe,CAAC,CAAiC;IACzD,OAAO,CAAC,iBAAiB,CAAC,CAAgB;IAC1C,OAAO,CAAC,mBAAmB,CAAC,CAAgB;IAC5C,OAAO,CAAC,mBAAmB,CAAC,CAAgB;IAC5C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,yBAAyB,CAAC,CAAkB;IACpD,OAAO,CAAC,oBAAoB,CAAS;IACrC,OAAO,CAAC,kBAAkB,CAAC,CAAQ;IACnC,OAAO,CAAC,gCAAgC,CAAK;IAE7C;;;OAGG;IACH,YAAqB,OAAO,EAAE,gBAAgB,EA8C7C;IAED,kBAAkB;IACZ,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,uBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC,CAwDrH;IAED,kBAAkB;IACZ,cAAc,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC,CAwBhD;IAED,kBAAkB;IAClB,OAAO,IAAI,IAAI,CAqBd;YAEa,gBAAgB;YA4ChB,eAAe;YAuBf,SAAS;IAmDvB;;;;OAIG;YACW,OAAO;YAcP,aAAa;IAyB3B,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,2BAA2B;IAOnC,OAAO,CAAC,eAAe;IA2BvB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,uBAAuB;YAQjB,cAAc;YAkBd,oBAAoB;IAmClC,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,0BAA0B;YAQpB,yBAAyB;CA4B1C"}
1
+ {"version":3,"file":"ChronicleClient.d.ts","sourceRoot":"","sources":["../ChronicleClient.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAUzD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAQ/C;;;;;;;;;;;;GAYG;AACH,qBAAa,eAAgB,YAAW,gBAAgB;IAwBxC,QAAQ,CAAC,OAAO,EAAE,gBAAgB;IAvB9C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,sBAAsB,CAAQ;IACtD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAU;IAE/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsB;IAClD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IAExD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAE1D,OAAO,CAAC,eAAe,CAAC,CAAiC;IACzD,OAAO,CAAC,iBAAiB,CAAC,CAAgB;IAC1C,OAAO,CAAC,mBAAmB,CAAC,CAAgB;IAC5C,OAAO,CAAC,mBAAmB,CAAC,CAAgB;IAC5C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,yBAAyB,CAAC,CAAkB;IACpD,OAAO,CAAC,oBAAoB,CAAS;IACrC,OAAO,CAAC,kBAAkB,CAAC,CAAQ;IACnC,OAAO,CAAC,gCAAgC,CAAK;IAE7C;;;OAGG;IACH,YAAqB,OAAO,EAAE,gBAAgB,EAiD7C;IAED,kBAAkB;IACZ,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,uBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC,CAuDrH;IAED,kBAAkB;IACZ,cAAc,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC,CAuBhD;IAED,kBAAkB;IAClB,OAAO,IAAI,IAAI,CAqBd;YAEa,gBAAgB;YA4ChB,eAAe;YAuBf,SAAS;IAmDvB;;;;OAIG;YACW,OAAO;YAcP,aAAa;IAyB3B,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,2BAA2B;IAOnC,OAAO,CAAC,eAAe;IA2BvB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,uBAAuB;YAQjB,cAAc;YAkBd,oBAAoB;IAmClC,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,0BAA0B;YAQpB,yBAAyB;CA4B1C"}
@@ -1,7 +1,9 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
  import * as os from 'os';
4
- import { diag } from '@opentelemetry/api';
4
+ import { createLogger } from './logging/createLogger.js';
5
+ import { setTelemetryAttribute, recordSafeException } from './telemetry/spanAttributes.js';
6
+ import { WellKnownTelemetryNames } from './WellKnownTelemetryNames.js';
5
7
  import { SpanStatusCode } from '@opentelemetry/api';
6
8
  import { ChronicleConnection } from './connection/index.js';
7
9
  import { IncompatibleChronicleServer } from './connection/IncompatibleChronicleServer.js';
@@ -15,7 +17,8 @@ import { EventStore } from './EventStore.js';
15
17
  import { EventStoreName } from './EventStoreName.js';
16
18
  import { EventStoreNamespaceName } from './EventStoreNamespaceName.js';
17
19
  import { ChronicleMetrics } from './Metrics.js';
18
- import { ChronicleTracer } from './Tracing.js';
20
+ import { observeOperation } from './telemetry/observeOperation.js';
21
+ import { runInBackgroundContext } from './telemetry/runInBackgroundContext.js';
19
22
  import { TypeDiscoverer } from './types/index.js';
20
23
  import { takeUnregisteredModelBoundMappings } from './types/modelBoundPropertyMetadata.js';
21
24
  import { reachableReadModelTypes } from './readModels/rootReadModelTypes.js';
@@ -39,9 +42,7 @@ export class ChronicleClient {
39
42
  _connection;
40
43
  _stores = new Map();
41
44
  _lifecycle = new ConnectionLifecycle();
42
- _logger = diag.createComponentLogger({
43
- namespace: '@cratis/chronicle/ChronicleClient'
44
- });
45
+ _logger;
45
46
  _watchdogHandle;
46
47
  _connectOperation;
47
48
  _reconnectOperation;
@@ -57,6 +58,7 @@ export class ChronicleClient {
57
58
  */
58
59
  constructor(options) {
59
60
  this.options = options;
61
+ this._logger = createLogger('@cratis/chronicle/ChronicleClient', options.logger);
60
62
  // When TLS is disabled, pass pre-built insecure credentials directly.
61
63
  // @cratis/chronicle.contracts ≤15.24.3 always composes call credentials with
62
64
  // channel credentials, which gRPC forbids for insecure channels. Passing
@@ -65,15 +67,17 @@ export class ChronicleClient {
65
67
  const connectionOptions = options.connectionString.disableTls
66
68
  ? { connectionString: options.connectionString, credentials: options.connectionString.createCredentials() }
67
69
  : { connectionString: options.connectionString };
68
- this._connection = new ChronicleConnection(connectionOptions);
70
+ this._connection = new ChronicleConnection({ ...connectionOptions, logger: options.logger });
69
71
  this._logger.info('Created Chronicle client', {
70
72
  serverAddress: `${options.connectionString.serverAddress.host}:${options.connectionString.serverAddress.port}`,
71
73
  disableTls: options.connectionString.disableTls
72
74
  });
73
75
  if (options.discoveryPatterns.length > 0) {
74
- this._discoveryOperation = TypeDiscoverer.default.discover(options.discoveryPatterns);
75
- this._discoveryOperation.catch(error => {
76
- this._logger.error('Artifact file discovery failed', { error: String(error) });
76
+ runInBackgroundContext(() => {
77
+ this._discoveryOperation = TypeDiscoverer.default.discover(options.discoveryPatterns);
78
+ this._discoveryOperation.catch(error => {
79
+ this._logger.error('Artifact file discovery failed', { error });
80
+ });
77
81
  });
78
82
  }
79
83
  this._lifecycle.onConnected(async () => {
@@ -104,9 +108,9 @@ export class ChronicleClient {
104
108
  : typeof namespace === 'string'
105
109
  ? new EventStoreNamespaceName(namespace)
106
110
  : namespace;
107
- return ChronicleTracer.startActiveSpan('chronicle.client.get_event_store', async (span) => {
108
- span.setAttribute('chronicle.event_store', storeName.value);
109
- span.setAttribute('chronicle.namespace', namespaceName.value);
111
+ return observeOperation(WellKnownTelemetryNames.spans.getEventStore, async (span) => {
112
+ setTelemetryAttribute(span, 'eventStore', storeName.value);
113
+ setTelemetryAttribute(span, 'namespace', namespaceName.value);
110
114
  try {
111
115
  const store = await this.withReconnect('get_event_store', async () => {
112
116
  await this.ensureConnected();
@@ -123,7 +127,7 @@ export class ChronicleClient {
123
127
  eventStore: storeName.value
124
128
  });
125
129
  ensureCommandSuccess('ensure event store', await this._connection.eventStores.ensureEventStore({ Name: storeName.value }));
126
- const created = new EventStore(storeName, namespaceName, this._connection, this._lifecycle, this.options.defaultSinkTypeId, this.options.clientArtifactsProvider, this.options.reactorResultHandler, this.options.artifactActivator, this.options.readModelNamingPolicy);
130
+ const created = new EventStore(storeName, namespaceName, this._connection, this._lifecycle, this.options.defaultSinkTypeId, this.options.clientArtifactsProvider, this.options.reactorResultHandler, this.options.artifactActivator, this.options.readModelNamingPolicy, this.options.telemetry);
127
131
  this._stores.set(key, created);
128
132
  await this.registerArtifactsForStore(created, 'new-store');
129
133
  return created;
@@ -136,12 +140,11 @@ export class ChronicleClient {
136
140
  return store;
137
141
  }
138
142
  catch (error) {
139
- span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
140
- span.recordException(error);
143
+ recordSafeException(span, error);
141
144
  this._logger.error('Failed getting event store', {
142
145
  eventStore: storeName.value,
143
146
  namespace: namespaceName.value,
144
- error: this.toErrorMessage(error)
147
+ error
145
148
  });
146
149
  throw error;
147
150
  }
@@ -152,7 +155,7 @@ export class ChronicleClient {
152
155
  }
153
156
  /** @inheritdoc */
154
157
  async getEventStores() {
155
- return ChronicleTracer.startActiveSpan('chronicle.client.get_event_stores', async (span) => {
158
+ return observeOperation(WellKnownTelemetryNames.spans.getEventStores, async (span) => {
156
159
  try {
157
160
  const response = await this.withReconnect('get_event_stores', async () => {
158
161
  await this.ensureConnected();
@@ -166,10 +169,9 @@ export class ChronicleClient {
166
169
  return result;
167
170
  }
168
171
  catch (error) {
169
- span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
170
- span.recordException(error);
172
+ recordSafeException(span, error);
171
173
  this._logger.error('Failed retrieving event stores', {
172
- error: this.toErrorMessage(error)
174
+ error
173
175
  });
174
176
  throw error;
175
177
  }
@@ -190,7 +192,7 @@ export class ChronicleClient {
190
192
  if (this._lifecycle.isConnected) {
191
193
  void this._lifecycle.disconnected(error => {
192
194
  this._logger.error('Disconnected lifecycle callback failed during dispose', {
193
- error: this.toErrorMessage(error)
195
+ error
194
196
  });
195
197
  });
196
198
  }
@@ -218,7 +220,7 @@ export class ChronicleClient {
218
220
  await this.startKernelKeepAlive();
219
221
  await this._lifecycle.connected(error => {
220
222
  this._logger.error('Connected lifecycle callback failed', {
221
- error: this.toErrorMessage(error)
223
+ error
222
224
  });
223
225
  });
224
226
  return;
@@ -249,7 +251,7 @@ export class ChronicleClient {
249
251
  return;
250
252
  }
251
253
  if (!this._connectOperation) {
252
- this._connectOperation = this.connectWithRetry().finally(() => {
254
+ this._connectOperation = runInBackgroundContext(() => this.connectWithRetry()).finally(() => {
253
255
  this._connectOperation = undefined;
254
256
  });
255
257
  }
@@ -259,15 +261,15 @@ export class ChronicleClient {
259
261
  if (this._connectionFailure)
260
262
  throw this._connectionFailure;
261
263
  if (!this._reconnectOperation) {
262
- this._reconnectOperation = (async () => {
264
+ this._reconnectOperation = runInBackgroundContext(async () => {
263
265
  this._logger.warn('Reconnecting to Chronicle kernel', {
264
266
  reason,
265
- error: this.toErrorMessage(error)
267
+ error
266
268
  });
267
269
  if (this._lifecycle.isConnected) {
268
270
  await this._lifecycle.disconnected(disconnectError => {
269
271
  this._logger.error('Disconnected lifecycle callback failed', {
270
- error: this.toErrorMessage(disconnectError)
272
+ error: disconnectError
271
273
  });
272
274
  });
273
275
  }
@@ -282,7 +284,7 @@ export class ChronicleClient {
282
284
  await this.startKernelKeepAlive();
283
285
  await this._lifecycle.connected(connectedError => {
284
286
  this._logger.error('Connected lifecycle callback failed after reconnect', {
285
- error: this.toErrorMessage(connectedError)
287
+ error: connectedError
286
288
  });
287
289
  });
288
290
  return;
@@ -297,7 +299,7 @@ export class ChronicleClient {
297
299
  await this.backOff(attempt, 'Reconnect attempt failed, retrying', reconnectError);
298
300
  }
299
301
  }
300
- })().finally(() => {
302
+ }).finally(() => {
301
303
  this._reconnectOperation = undefined;
302
304
  });
303
305
  }
@@ -314,7 +316,7 @@ export class ChronicleClient {
314
316
  this._logger.warn(message, {
315
317
  attempt,
316
318
  delayMs,
317
- error: this.toErrorMessage(error)
319
+ error
318
320
  });
319
321
  await new Promise(resolve => setTimeout(resolve, delayMs));
320
322
  return delayMs;
@@ -394,9 +396,9 @@ export class ChronicleClient {
394
396
  return String(error);
395
397
  }
396
398
  startConnectionWatchdog() {
397
- this._watchdogHandle = setInterval(() => {
399
+ this._watchdogHandle = runInBackgroundContext(() => setInterval(() => {
398
400
  void this.runHealthCheck().catch(error => this.backgroundConnectionFailed('watchdog-health-check', error));
399
- }, ChronicleClient._healthCheckIntervalMs);
401
+ }, ChronicleClient._healthCheckIntervalMs));
400
402
  this._watchdogHandle.unref?.();
401
403
  }
402
404
  async runHealthCheck() {
@@ -430,7 +432,7 @@ export class ChronicleClient {
430
432
  return;
431
433
  }
432
434
  void this.reconnect(reason, error).catch(failure => this.backgroundConnectionFailed(reason, failure));
433
- });
435
+ }, {}, this.options.logger);
434
436
  await keepAlive.start({
435
437
  ConnectionId: this._lifecycle.connectionId,
436
438
  ClientVersion: clientVersion,
@@ -457,7 +459,7 @@ export class ChronicleClient {
457
459
  this.failConnection(error instanceof Error ? error : new Error(String(error)));
458
460
  this._logger.error('Background connection recovery failed; create a new client after correcting the server', {
459
461
  reason,
460
- error: this.toErrorMessage(error)
462
+ error
461
463
  });
462
464
  }
463
465
  async registerArtifactsForStore(store, reason) {