@cratis/chronicle 6.33.0 → 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 (162) 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/reducers/Reducers.d.ts.map +1 -1
  110. package/dist/reducers/Reducers.js +28 -25
  111. package/dist/reducers/Reducers.js.map +1 -1
  112. package/dist/seeding/EventSeeding.d.ts.map +1 -1
  113. package/dist/seeding/EventSeeding.js +4 -3
  114. package/dist/seeding/EventSeeding.js.map +1 -1
  115. package/dist/telemetry/ChronicleTelemetryOptions.d.ts +11 -0
  116. package/dist/telemetry/ChronicleTelemetryOptions.d.ts.map +1 -0
  117. package/dist/telemetry/ChronicleTelemetryOptions.js +4 -0
  118. package/dist/telemetry/ChronicleTelemetryOptions.js.map +1 -0
  119. package/dist/telemetry/observeOperation.d.ts +6 -0
  120. package/dist/telemetry/observeOperation.d.ts.map +1 -0
  121. package/dist/telemetry/observeOperation.js +23 -0
  122. package/dist/telemetry/observeOperation.js.map +1 -0
  123. package/dist/telemetry/runInBackgroundContext.d.ts +3 -0
  124. package/dist/telemetry/runInBackgroundContext.d.ts.map +1 -0
  125. package/dist/telemetry/runInBackgroundContext.js +9 -0
  126. package/dist/telemetry/runInBackgroundContext.js.map +1 -0
  127. package/dist/telemetry/spanAttributes.d.ts +14 -0
  128. package/dist/telemetry/spanAttributes.d.ts.map +1 -0
  129. package/dist/telemetry/spanAttributes.js +47 -0
  130. package/dist/telemetry/spanAttributes.js.map +1 -0
  131. package/dist/tsconfig.tsbuildinfo +1 -1
  132. package/eventSequences/EventLog.ts +4 -2
  133. package/eventSequences/EventSequence.ts +64 -81
  134. package/eventStoreSubscriptions/EventStoreSubscriptions.ts +5 -5
  135. package/for_ChronicleClient/when_starting_background_work/with_a_correlated_caller.spec.ts +150 -0
  136. package/for_ChronicleOptions/when_configuring_telemetry/with_event_source_identifiers.spec.ts +44 -0
  137. package/index.ts +7 -1
  138. package/logging/ChronicleLogEntry.ts +14 -0
  139. package/logging/ChronicleLogLevel.ts +11 -0
  140. package/logging/DiagChronicleLogger.ts +13 -0
  141. package/logging/IChronicleLogger.ts +10 -0
  142. package/logging/createLogger.ts +50 -0
  143. package/logging/for_ChronicleLogger/when_logging/with_client_owned_sinks.spec.ts +96 -0
  144. package/package.json +7 -1
  145. package/projections/Projections.ts +4 -3
  146. package/reactors/Reactors.ts +27 -25
  147. package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +14 -14
  148. package/reducers/Reducers.ts +29 -26
  149. package/seeding/EventSeeding.ts +6 -4
  150. package/telemetry/ChronicleTelemetryOptions.ts +8 -0
  151. package/telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.ts +36 -0
  152. package/telemetry/for_ChronicleTelemetry/given/an_event_sequence.fixture.ts +29 -0
  153. package/telemetry/for_ChronicleTelemetry/when_appending/with_shared_and_legacy_attributes.spec.ts +58 -0
  154. package/telemetry/for_ChronicleTelemetry/when_appending/without_an_sdk.spec.ts +15 -0
  155. package/telemetry/for_ChronicleTelemetry/when_documenting/with_public_names.spec.ts +15 -0
  156. package/telemetry/for_ChronicleTelemetry/when_recording_failures/with_sensitive_exception_details.spec.ts +34 -0
  157. package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts +92 -0
  158. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_legacy_overlap.spec.ts +40 -0
  159. package/telemetry/for_ChronicleTelemetry/when_recording_sequence_numbers/with_integer_precision.spec.ts +23 -0
  160. package/telemetry/observeOperation.ts +25 -0
  161. package/telemetry/runInBackgroundContext.ts +10 -0
  162. package/telemetry/spanAttributes.ts +49 -0
@@ -0,0 +1,150 @@
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 } 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, EventStoresDefinition, ServerDefinition, ReactorsDefinition, ReducersDefinition, ReadModelsDefinition } from '@cratis/chronicle.contracts';
9
+ import { ChronicleClient } from '../../ChronicleClient.js';
10
+ import { ChronicleOptions } from '../../ChronicleOptions.js';
11
+ import type { IClientArtifactsProvider } from '../../artifacts/IClientArtifactsProvider.js';
12
+ import { CorrelationId, correlationIdManager } from '../../correlation/index.js';
13
+ import type { ChronicleLogEntry } from '../../logging/ChronicleLogEntry.js';
14
+ import { reactor } from '../../reactors/reactor.js';
15
+ import { reducer } from '../../reducers/reducer.js';
16
+ import { telemetrySession } from '../../telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.js';
17
+
18
+ const should = chai.should();
19
+ const telemetry = telemetrySession();
20
+ class BackgroundReactor {}
21
+ reactor('background-reactor')(BackgroundReactor);
22
+ class BackgroundReducer {}
23
+ reducer('background-reducer')(BackgroundReducer);
24
+ const artifacts: IClientArtifactsProvider = {
25
+ eventTypes: [], readModels: [], reactors: [BackgroundReactor], reducers: [BackgroundReducer],
26
+ seeders: [], constraints: [], projections: [], webhooks: [], eventTypeMigrations: [], globalForHandlers: []
27
+ };
28
+ let server: ReturnType<typeof createServer>;
29
+ let client: ChronicleClient;
30
+ let received: { method: string; metadata: Metadata }[];
31
+ let logs: ChronicleLogEntry[];
32
+ let hostTraceId: string;
33
+ const correlation = new CorrelationId('a4450131-7cd4-40ba-9438-69c680efc1f3');
34
+
35
+ function deferred() {
36
+ let resolve!: () => void;
37
+ const promise = new Promise<void>(complete => { resolve = complete; });
38
+ return { promise, resolve };
39
+ }
40
+
41
+ beforeEach(async () => {
42
+ received = [];
43
+ logs = [];
44
+ const reobserved = deferred();
45
+ const reconnected = deferred();
46
+ const endKeepAlive = deferred();
47
+ const counts = new Map<string, number>();
48
+ server = createServer().use(async function* (call, callContext) {
49
+ const method = call.method.path;
50
+ received.push({ method, metadata: Metadata(callContext.metadata) });
51
+ const count = (counts.get(method) ?? 0) + 1;
52
+ counts.set(method, count);
53
+ const observations = [...counts].filter(([path]) => path.endsWith('/Observe'));
54
+ if (observations.length === 2 && observations.every(([, total]) => total >= 2)) reobserved.resolve();
55
+ if (observations.length === 2 && observations.every(([, total]) => total >= 3)) reconnected.resolve();
56
+ return yield* call.next(call.request, callContext);
57
+ });
58
+ const unused = async () => { throw new Error('Unexpected RPC'); };
59
+ const success = async () => ({ IsAuthorized: true, ValidationResults: [], ExceptionMessages: [] });
60
+ let connections = 0;
61
+ server.add(ConnectionServiceDefinition, {
62
+ ...Object.fromEntries(Object.keys(ConnectionServiceDefinition.methods).map(name => [name, unused])),
63
+ checkCompatibility: async () => ({ IsCompatible: true, Incompatibilities: [], ServerVersion: 'test' }),
64
+ connectionKeepAlive: async () => ({}),
65
+ connect: async function* (request, callContext) {
66
+ const first = ++connections === 1;
67
+ yield { ConnectionId: request.ConnectionId };
68
+ if (first) await endKeepAlive.promise;
69
+ else await new Promise<void>(resolve => callContext.signal.addEventListener('abort', () => resolve(), { once: true }));
70
+ }
71
+ } as ServiceImplementation<typeof ConnectionServiceDefinition>);
72
+ server.add(ServerDefinition, {
73
+ ...Object.fromEntries(Object.keys(ServerDefinition.methods).map(name => [name, unused])),
74
+ getVersionInfo: async () => ({})
75
+ } as ServiceImplementation<typeof ServerDefinition>);
76
+ server.add(EventStoresDefinition, {
77
+ allEventStores: unused, ensureEventStore: success, observeEventStores: unused
78
+ } as ServiceImplementation<typeof EventStoresDefinition>);
79
+ server.add(ReadModelsDefinition, {
80
+ ...Object.fromEntries(Object.keys(ReadModelsDefinition.methods).map(name => [name, unused])),
81
+ registerMany: success
82
+ } as ServiceImplementation<typeof ReadModelsDefinition>);
83
+ server.add(ReactorsDefinition, {
84
+ ...Object.fromEntries(Object.keys(ReactorsDefinition.methods).map(name => [name, unused])),
85
+ observe: async function* (requests) {
86
+ for await (const _request of requests) {
87
+ yield { Events: [], Partition: '', ReplayState: 0 };
88
+ throw new ServerError(Status.UNAVAILABLE, 'End stream to exercise re-observation');
89
+ }
90
+ }
91
+ } as ServiceImplementation<typeof ReactorsDefinition>);
92
+ server.add(ReducersDefinition, {
93
+ ...Object.fromEntries(Object.keys(ReducersDefinition.methods).map(name => [name, unused])),
94
+ observe: async function* (requests) {
95
+ for await (const _request of requests) {
96
+ yield { Events: [], Partition: '', ReplayState: 0 };
97
+ throw new ServerError(Status.UNAVAILABLE, 'End stream to exercise re-observation');
98
+ }
99
+ }
100
+ } as ServiceImplementation<typeof ReducersDefinition>);
101
+ const port = await server.listen('127.0.0.1:0');
102
+ const span = telemetry.traces.getTracer('host').startSpan('host');
103
+ hostTraceId = span.spanContext().traceId;
104
+ const active = propagation.setBaggage(trace.setSpan(context.active(), span), propagation.createBaggage({
105
+ 'cratis.correlation_id': { value: correlation.toString() }, email: { value: 'private@example.com' }
106
+ }));
107
+ try {
108
+ await context.with(active, () => correlationIdManager.run(correlation, async () => {
109
+ client = new ChronicleClient(ChronicleOptions.fromConnectionString(`chronicle://127.0.0.1:${port}?disableTls=true&apiKey=test`, {
110
+ discoveryPatterns: [], clientArtifactsProvider: artifacts, logger: { log: entry => logs.push(entry) }
111
+ }));
112
+ await client.getEventStore('store');
113
+ context.active().should.equal(active);
114
+ correlationIdManager.scoped!.should.equal(correlation);
115
+ }));
116
+ await reobserved.promise;
117
+ endKeepAlive.resolve();
118
+ await reconnected.promise;
119
+ } finally { span.end(); endKeepAlive.resolve(); }
120
+ });
121
+
122
+ afterEach(async () => {
123
+ client?.dispose();
124
+ await server?.shutdown();
125
+ });
126
+
127
+ describe('when a correlated getEventStore starts background streams and their recovery loops', () => {
128
+ it('should not propagate the caller trace or baggage on observations or keep-alive RPCs', () => {
129
+ const background = received.filter(({ method }) => /\/(Observe|Connect|ConnectionKeepAlive)$/.test(method));
130
+ background.length.should.be.greaterThanOrEqual(10);
131
+ for (const { metadata } of background) {
132
+ should.equal(metadata.get('traceparent'), undefined);
133
+ should.equal(metadata.get('baggage'), undefined);
134
+ }
135
+ });
136
+ it('should retain the caller context on the foreground operation', () => {
137
+ const foreground = received.find(({ method }) => method.endsWith('/EnsureEventStore'))!;
138
+ foreground.metadata.get('traceparent')!.should.contain(hostTraceId);
139
+ should.equal(foreground.metadata.get('baggage'), `cratis.correlation_id=${correlation}`);
140
+ });
141
+ it('should not label background-loop diagnostics with the initiating business correlation or trace', () => {
142
+ const background = logs.filter(entry => /observation|keep-alive|Reconnect/.test(entry.message));
143
+ background.length.should.be.greaterThan(0);
144
+ for (const entry of background) {
145
+ entry.attributes.should.not.have.property('cratis.correlation_id');
146
+ entry.attributes.should.not.have.property('trace_id');
147
+ entry.attributes.should.not.have.property('span_id');
148
+ }
149
+ });
150
+ });
@@ -0,0 +1,44 @@
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 { chai, describe, it } from 'vitest';
5
+ import { runInNewContext } from 'node:vm';
6
+ import { ChronicleOptions } from '../../ChronicleOptions.js';
7
+ import type { ChronicleTelemetryOptions } from '../../telemetry/ChronicleTelemetryOptions.js';
8
+
9
+ chai.should();
10
+
11
+ for (const policy of [
12
+ { mode: 'unknown' }, {}, null,
13
+ { mode: 'hmac' }, { mode: 'hmac', key: '' }, { mode: 'hmac', key: 'secret' },
14
+ { mode: 'hmac', key: [] }, { mode: 'hmac', key: new Uint8Array() },
15
+ { mode: 'hmac', key: Buffer.alloc(0) }, { mode: 'hmac', key: new Uint16Array([1]) },
16
+ { mode: 'hmac', key: new DataView(new ArrayBuffer(1)) },
17
+ { mode: 'hmac', key: runInNewContext('new Uint8Array()') },
18
+ { mode: 'hmac', key: runInNewContext('new Uint16Array([1])') }
19
+ ]) {
20
+ describe(`when configuring event source privacy with ${JSON.stringify(policy)}`, () => {
21
+ it('should reject invalid telemetry configuration before a client can make business calls', () => {
22
+ const telemetry = { eventSourceId: policy } as unknown as ChronicleTelemetryOptions;
23
+ (() => ChronicleOptions.development({ telemetry })).should.throw(TypeError);
24
+ (() => ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { telemetry })).should.throw(TypeError);
25
+ });
26
+ });
27
+ }
28
+
29
+ describe('when configuring an HMAC key created in another realm', () => {
30
+ it('should accept a non-empty byte array without relying on its realm constructor', () => {
31
+ const key = runInNewContext('new Uint8Array([1, 2, 3])') as Uint8Array;
32
+ (key instanceof Uint8Array).should.be.false;
33
+ (() => ChronicleOptions.development({ telemetry: { eventSourceId: { mode: 'hmac', key } } })).should.not.throw();
34
+ (() => ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { telemetry: { eventSourceId: { mode: 'hmac', key } } })).should.not.throw();
35
+ });
36
+ });
37
+
38
+ for (const eventSourceId of [undefined, { mode: 'raw' }, { mode: 'hmac', key: new Uint8Array([1]) }, { mode: 'hmac', key: Buffer.from('key') }] as const) {
39
+ describe('when configuring a supported event source privacy policy', () => {
40
+ it('should accept omitted identifiers, raw identifiers and non-empty byte keys', () => {
41
+ (() => ChronicleOptions.development({ telemetry: { eventSourceId } })).should.not.throw();
42
+ });
43
+ });
44
+ }
package/index.ts CHANGED
@@ -37,7 +37,13 @@ export { EventStoreNamespaceName } from './EventStoreNamespaceName.js';
37
37
  export { Guid } from '@cratis/fundamentals';
38
38
  export { ReplayState } from '@cratis/chronicle.contracts';
39
39
  export { ChronicleInstrumentationName, ChronicleTracer } from './Tracing.js';
40
- export { ChronicleMeter, ChronicleMeterName, ChronicleMetrics } from './Metrics.js';
40
+ export { ChronicleMeter, ChronicleMeterName, ChronicleMetrics, ChronicleConventionMetrics } from './Metrics.js';
41
+ export { WellKnownTelemetryNames } from './WellKnownTelemetryNames.js';
42
+ export type { ChronicleTelemetryOptions } from './telemetry/ChronicleTelemetryOptions.js';
43
+ export type { IChronicleLogger } from './logging/IChronicleLogger.js';
44
+ export type { ChronicleLogEntry } from './logging/ChronicleLogEntry.js';
45
+ export { ChronicleLogLevel } from './logging/ChronicleLogLevel.js';
46
+ export { DiagChronicleLogger } from './logging/DiagChronicleLogger.js';
41
47
 
42
48
  export * from './events/index.js';
43
49
  export * from './eventSequences/index.js';
@@ -0,0 +1,14 @@
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 type { Attributes } from '@opentelemetry/api';
5
+ import type { ChronicleLogLevel } from './ChronicleLogLevel.js';
6
+
7
+ /** Structured diagnostic without event payloads, error messages or error stacks. */
8
+ export interface ChronicleLogEntry {
9
+ readonly category: string;
10
+ readonly level: ChronicleLogLevel;
11
+ readonly message: string;
12
+ /** Safe diagnostic fields, including correlation and valid trace/span identifiers when available. */
13
+ readonly attributes: Readonly<Attributes>;
14
+ }
@@ -0,0 +1,11 @@
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
+ /** Severity of a Chronicle diagnostic. */
5
+ export enum ChronicleLogLevel {
6
+ Verbose = 'verbose',
7
+ Debug = 'debug',
8
+ Info = 'info',
9
+ Warn = 'warn',
10
+ Error = 'error'
11
+ }
@@ -0,0 +1,13 @@
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 { diag } from '@opentelemetry/api';
5
+ import type { IChronicleLogger } from './IChronicleLogger.js';
6
+ import type { ChronicleLogEntry } from './ChronicleLogEntry.js';
7
+
8
+ /** Compatibility adapter used when no application logger is supplied during the minor-release overlap. */
9
+ export class DiagChronicleLogger implements IChronicleLogger {
10
+ log(entry: ChronicleLogEntry): void {
11
+ diag.createComponentLogger({ namespace: entry.category })[entry.level](entry.message, entry.attributes);
12
+ }
13
+ }
@@ -0,0 +1,10 @@
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 type { ChronicleLogEntry } from './ChronicleLogEntry.js';
5
+
6
+ /** Application-owned diagnostic sink, configured separately for each Chronicle client. */
7
+ export interface IChronicleLogger {
8
+ /** Receives one sanitized diagnostic. Implementations should not throw. */
9
+ log(entry: ChronicleLogEntry): void;
10
+ }
@@ -0,0 +1,50 @@
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 { context, propagation, trace, isSpanContextValid, type Attributes } from '@opentelemetry/api';
5
+ import { ClientError, ServerError } from 'nice-grpc-common';
6
+ import { correlationIdManager } from '../correlation/index.js';
7
+ import { WellKnownTelemetryNames } from '../WellKnownTelemetryNames.js';
8
+ import { exceptionType } from '../telemetry/spanAttributes.js';
9
+ import { ChronicleLogLevel } from './ChronicleLogLevel.js';
10
+ import { DiagChronicleLogger } from './DiagChronicleLogger.js';
11
+ import type { IChronicleLogger } from './IChronicleLogger.js';
12
+
13
+ /** Binds a category to a client-owned sink and adds safe correlation fields. */
14
+ export function createLogger(category: string, sink: IChronicleLogger = new DiagChronicleLogger()) {
15
+ const log = (level: ChronicleLogLevel) => (message: string, fields: Record<string, unknown> = {}) => {
16
+ const attributes: Attributes = {};
17
+ for (const [key, value] of Object.entries(fields)) {
18
+ if (key === 'error') {
19
+ attributes['error.type'] = exceptionType(value);
20
+ attributes['exception.type'] = exceptionType(value);
21
+ if (value instanceof ClientError || value instanceof ServerError) attributes['rpc.grpc.status_code'] = value.code;
22
+ } else if (key === 'sequenceNumber') {
23
+ const number = Number(value);
24
+ if (Number.isSafeInteger(number)) attributes[WellKnownTelemetryNames.attributes.sequenceNumber] = number;
25
+ } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
26
+ attributes[key] = value;
27
+ } else if (Array.isArray(value) && value.every(item => typeof item === 'string')) {
28
+ attributes[key] = value;
29
+ }
30
+ }
31
+ const active = context.active();
32
+ const correlationKey = WellKnownTelemetryNames.attributes.correlationId;
33
+ const correlation = correlationIdManager.scoped?.toString() ?? propagation.getBaggage(active)?.getEntry(correlationKey)?.value;
34
+ if (correlation) attributes[correlationKey] = correlation;
35
+ const spanContext = trace.getSpanContext(active);
36
+ if (spanContext && isSpanContextValid(spanContext)) {
37
+ attributes.trace_id = spanContext.traceId;
38
+ attributes.span_id = spanContext.spanId;
39
+ }
40
+ try {
41
+ sink.log({ category, level, message, attributes });
42
+ } catch {
43
+ // Diagnostics are not the operation's outcome. A broken sink must not change RPCs or acknowledgements.
44
+ }
45
+ };
46
+ return {
47
+ verbose: log(ChronicleLogLevel.Verbose), debug: log(ChronicleLogLevel.Debug),
48
+ info: log(ChronicleLogLevel.Info), warn: log(ChronicleLogLevel.Warn), error: log(ChronicleLogLevel.Error)
49
+ };
50
+ }
@@ -0,0 +1,96 @@
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 { beforeEach, chai, describe, it, vi } from 'vitest';
5
+ import { diag, DiagLogLevel } from '@opentelemetry/api';
6
+ import { ClientError, ServerError, Status } from 'nice-grpc-common';
7
+ import { ChronicleClient } from '../../../ChronicleClient.js';
8
+ import { ChronicleOptions } from '../../../ChronicleOptions.js';
9
+ import { createLogger } from '../../createLogger.js';
10
+ import type { ChronicleLogEntry } from '../../ChronicleLogEntry.js';
11
+ import { telemetrySession } from '../../../telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.js';
12
+ import { CorrelationId, correlationIdManager } from '../../../correlation/index.js';
13
+
14
+ vi.mock('../../../connection/ChronicleConnection.js', () => ({ ChronicleConnection: class { disconnect() {} } }));
15
+ chai.should();
16
+ const telemetry = telemetrySession();
17
+
18
+ describe('when two clients have different diagnostic sinks', () => {
19
+ const first: ChronicleLogEntry[] = [];
20
+ const second: ChronicleLogEntry[] = [];
21
+ beforeEach(() => {
22
+ first.length = 0;
23
+ second.length = 0;
24
+ const one = new ChronicleClient(ChronicleOptions.development({ discoveryPatterns: [], logger: { log: entry => first.push(entry) } }));
25
+ const two = new ChronicleClient(ChronicleOptions.development({ discoveryPatterns: [], logger: { log: entry => second.push(entry) } }));
26
+ one.dispose();
27
+ two.dispose();
28
+ });
29
+ it('should route each diagnostic only to its owning sink', () => {
30
+ first.should.have.lengthOf(2);
31
+ second.should.have.lengthOf(2);
32
+ (first[0] === second[0]).should.be.false;
33
+ });
34
+ });
35
+
36
+ describe('when logging in a correlated trace', () => {
37
+ let entry: ChronicleLogEntry;
38
+ beforeEach(async () => {
39
+ const logger = createLogger('test', { log: value => { entry = value; } });
40
+ await correlationIdManager.run(new CorrelationId('business-id'), () => telemetry.traces.getTracer('host').startActiveSpan('operation', async span => {
41
+ try { logger.error('Operation failed', { error: new TypeError('secret'), sequenceNumber: 9007199254740992n }); }
42
+ finally { span.end(); }
43
+ }));
44
+ });
45
+ it('should export error types with business and trace identifiers but no exception details', () => {
46
+ entry.attributes.should.include({ 'error.type': 'TypeError', 'exception.type': 'TypeError', 'cratis.correlation_id': 'business-id' });
47
+ entry.attributes.should.have.property('trace_id');
48
+ entry.attributes.should.have.property('span_id');
49
+ entry.attributes.should.not.have.property('cratis.event_sequence.number');
50
+ JSON.stringify(entry).should.not.contain('secret');
51
+ });
52
+ });
53
+
54
+ for (const error of [new ClientError('/test', Status.UNAVAILABLE, 'secret'), new ServerError(Status.UNAVAILABLE, 'secret')]) {
55
+ describe('when logging a gRPC failure', () => {
56
+ let entry: ChronicleLogEntry;
57
+ beforeEach(() => {
58
+ createLogger('test', { log: value => { entry = value; } }).error('Operation failed', { error });
59
+ });
60
+ it('should retain the numeric status without exporting sensitive exception details', () => {
61
+ entry.attributes.should.include({ 'rpc.grpc.status_code': Status.UNAVAILABLE });
62
+ JSON.stringify(entry).should.not.contain('secret');
63
+ });
64
+ });
65
+ }
66
+
67
+ for (const error of [new DOMException('secret', 'TimeoutError'), new DOMException('secret', 'AbortError'), { code: Status.UNAVAILABLE, message: 'secret' }]) {
68
+ describe('when logging a non-gRPC error with a numeric code', () => {
69
+ let entry: ChronicleLogEntry;
70
+ beforeEach(() => {
71
+ createLogger('test', { log: value => { entry = value; } }).error('Operation failed', { error });
72
+ });
73
+ it('should not label its code as a gRPC status', () => {
74
+ entry.attributes.should.not.have.property('rpc.grpc.status_code');
75
+ JSON.stringify(entry).should.not.contain('secret');
76
+ });
77
+ });
78
+ }
79
+
80
+ describe('when no logger is configured during the compatibility period', () => {
81
+ it('should still deliver sanitized diagnostics to diag', () => {
82
+ const error = vi.fn();
83
+ diag.setLogger({ error, warn: vi.fn(), info: vi.fn(), debug: vi.fn(), verbose: vi.fn() }, DiagLogLevel.ERROR);
84
+ try {
85
+ createLogger('test').error('Operation failed', { error: new Error('secret') });
86
+ error.mock.calls.should.have.lengthOf(1);
87
+ JSON.stringify(error.mock.calls).should.not.contain('secret');
88
+ } finally { diag.disable(); }
89
+ });
90
+ });
91
+
92
+ describe('when a diagnostic sink throws', () => {
93
+ it('should leave the operation unaffected', () => {
94
+ (() => createLogger('test', { log: () => { throw new Error('sink failed'); } }).info('Diagnostic')).should.not.throw();
95
+ });
96
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/chronicle",
3
- "version": "6.33.0",
3
+ "version": "6.34.0",
4
4
  "description": "Event sourcing for TypeScript and Node.js — the idiomatic client for Cratis Chronicle, the open-source event-sourcing database",
5
5
  "author": "Cratis",
6
6
  "license": "MIT",
@@ -184,6 +184,12 @@
184
184
  },
185
185
  "devDependencies": {
186
186
  "@cratis/fundamentals": "7.20.0",
187
+ "@opentelemetry/context-async-hooks": "2.11.0",
188
+ "@opentelemetry/core": "2.11.0",
189
+ "@opentelemetry/instrumentation-grpc": "0.222.0",
190
+ "@opentelemetry/instrumentation-http": "0.222.0",
191
+ "@opentelemetry/sdk-metrics": "2.11.0",
192
+ "@opentelemetry/sdk-trace-base": "2.11.0",
187
193
  "@types/node": "^26.6.3",
188
194
  "@typescript/native": "npm:typescript@^7.0.2",
189
195
  "esbuild": "^0.28.2",
@@ -1,7 +1,7 @@
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
 
4
- import { diag } from '@opentelemetry/api';
4
+ import { createLogger } from '../logging/createLogger.js';
5
5
  import { ProjectionOwner, type ProjectionDefinition } from '@cratis/chronicle.contracts';
6
6
  import { Constructor, Guid } from '@cratis/fundamentals';
7
7
  import { IClientArtifactsProvider } from '../artifacts/index.js';
@@ -36,7 +36,7 @@ export class Projections implements IProjections {
36
36
  private readonly _modelBound = new Map<string, Constructor>();
37
37
  private readonly _failedPartitions: FailedPartitions;
38
38
 
39
- private readonly _logger = diag.createComponentLogger({ namespace: '@cratis/chronicle/projections' });
39
+ private readonly _logger: ReturnType<typeof createLogger>;
40
40
 
41
41
  /**
42
42
  * Creates a new {@link Projections} instance.
@@ -55,6 +55,7 @@ export class Projections implements IProjections {
55
55
  private readonly _defaultSinkTypeId: string,
56
56
  private readonly _readModelNamingPolicy?: ReadModelNamingPolicy
57
57
  ) {
58
+ this._logger = createLogger('@cratis/chronicle/projections', _connection.logger);
58
59
  this._failedPartitions = new FailedPartitions(_eventStore, _namespace, _connection);
59
60
  }
60
61
 
@@ -296,7 +297,7 @@ export class Projections implements IProjections {
296
297
  identifier,
297
298
  attempt,
298
299
  nextAttemptInMs: delay,
299
- error: msg
300
+ error
300
301
  });
301
302
  await new Promise(resolve => setTimeout(resolve, delay));
302
303
  delay = Math.min(delay * 2, 15000);
@@ -2,7 +2,8 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import 'reflect-metadata';
5
- import { diag } from '@opentelemetry/api';
5
+ import { createLogger } from '../logging/createLogger.js';
6
+ import { runInBackgroundContext } from '../telemetry/runInBackgroundContext.js';
6
7
  import { Constructor } from '@cratis/fundamentals';
7
8
  import { ObservationState, ReactorMessage, ReplayState } from '@cratis/chronicle.contracts';
8
9
  import { IClientArtifactsProvider } from '../artifacts/index.js';
@@ -102,7 +103,7 @@ export class Reactors implements IReactors {
102
103
  /** How long to wait before re-establishing an observation whose stream ended. */
103
104
  private static readonly _reobserveDelayMs = 2000;
104
105
 
105
- private readonly _logger = diag.createComponentLogger({ namespace: '@cratis/chronicle/reactors' });
106
+ private readonly _logger: ReturnType<typeof createLogger>;
106
107
  private readonly _lifecycle: ConnectionLifecycle;
107
108
  private readonly _reactors = new Map<string, Constructor>();
108
109
  private readonly _queues = new Map<string, AsyncQueue<ReactorMessage>>();
@@ -130,6 +131,7 @@ export class Reactors implements IReactors {
130
131
  private readonly _eventStore?: IEventStore,
131
132
  private readonly _artifactActivator?: ClientArtifactsActivator
132
133
  ) {
134
+ this._logger = createLogger('@cratis/chronicle/reactors', _connection.logger);
133
135
  this._lifecycle = lifecycle;
134
136
  lifecycle.onDisconnected(async () => {
135
137
  this._logger.info('Disconnected — stopping all reactor observations');
@@ -177,19 +179,21 @@ export class Reactors implements IReactors {
177
179
  }
178
180
 
179
181
  private startObservation(id: string, reactorType: Constructor): void {
180
- if (this._disposed) return;
181
- const metadata = getReactorMetadata(reactorType)!;
182
- const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
183
- const eventTypes = getReactorEventTypes(reactorType, this._clientArtifacts.eventTypes);
184
-
185
- this._logger.info('Starting reactor observation', {
186
- reactorId: id,
187
- eventSequenceId,
188
- handlerCount: eventTypes.length,
189
- handlers: eventTypes.map(e => e.methodName)
182
+ runInBackgroundContext(() => {
183
+ if (this._disposed) return;
184
+ const metadata = getReactorMetadata(reactorType)!;
185
+ const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
186
+ const eventTypes = getReactorEventTypes(reactorType, this._clientArtifacts.eventTypes);
187
+
188
+ this._logger.info('Starting reactor observation', {
189
+ reactorId: id,
190
+ eventSequenceId,
191
+ handlerCount: eventTypes.length,
192
+ handlers: eventTypes.map(e => e.methodName)
193
+ });
194
+
195
+ void this.runObservation(id, reactorType, eventSequenceId, eventTypes);
190
196
  });
191
-
192
- void this.runObservation(id, reactorType, eventSequenceId, eventTypes);
193
197
  }
194
198
 
195
199
  private async runObservation(
@@ -201,7 +205,7 @@ export class Reactors implements IReactors {
201
205
  try {
202
206
  await this.observeReactor(id, reactorType, eventSequenceId, eventTypes);
203
207
  } catch (error) {
204
- this._logger.error('Reactor observation loop exited with error', { reactorId: id, error: String(error) });
208
+ this._logger.error('Reactor observation loop exited with error', { reactorId: id, error });
205
209
  }
206
210
 
207
211
  this.scheduleReobserve(id, reactorType);
@@ -223,14 +227,14 @@ export class Reactors implements IReactors {
223
227
  return;
224
228
  }
225
229
 
226
- const handle = setTimeout(() => {
230
+ const handle = runInBackgroundContext(() => setTimeout(() => {
227
231
  if (!this._registered || this._disposed) {
228
232
  return;
229
233
  }
230
234
 
231
235
  this._logger.info('Re-establishing reactor observation', { reactorId: id });
232
236
  this.startObservation(id, reactorType);
233
- }, Reactors._reobserveDelayMs);
237
+ }, Reactors._reobserveDelayMs));
234
238
 
235
239
  handle.unref?.();
236
240
  }
@@ -286,7 +290,6 @@ export class Reactors implements IReactors {
286
290
 
287
291
  this._logger.debug('Received events to observe', {
288
292
  reactorId: id,
289
- partition: eventsToObserve.Partition,
290
293
  count: eventsToObserve.Events.length,
291
294
  replayState: eventsToObserve.ReplayState
292
295
  });
@@ -301,12 +304,12 @@ export class Reactors implements IReactors {
301
304
  replayState: eventsToObserve.ReplayState
302
305
  }, this._artifactActivator, artifact => runActivated(artifact, () =>
303
306
  notifyReplayLifecycle(artifact.instance, eventsToObserve.ReplayState, eventsToObserve.Partition),
304
- { delivery: ArtifactDelivery.ReplayNotification, replayState: eventsToObserve.ReplayState }));
307
+ { delivery: ArtifactDelivery.ReplayNotification, replayState: eventsToObserve.ReplayState }), this._connection.logger);
305
308
  } else if (reactorInstance) {
306
309
  await notifyReplayLifecycle(reactorInstance, eventsToObserve.ReplayState, eventsToObserve.Partition);
307
310
  }
308
311
  } catch (err) {
309
- this._logger.error('Error notifying reactor of replay lifecycle transition', { reactorId: id, error: String(err) });
312
+ this._logger.error('Error notifying reactor of replay lifecycle transition', { reactorId: id, error: err });
310
313
  if (err instanceof ArtifactCompletionFailed && err.processingError !== undefined) {
311
314
  exceptionMessages.push(String(err.processingError));
312
315
  }
@@ -348,7 +351,6 @@ export class Reactors implements IReactors {
348
351
  }
349
352
 
350
353
  const content = JSON.parse(event.Content) as Record<string, unknown>;
351
- this._logger.debug('Event content', { reactorId: id, eventTypeId, contentKeys: Object.keys(content), rawContent: event.Content.substring(0, 200) });
352
354
  const context = toClientEventContext(event.Context!);
353
355
 
354
356
  this._logger.info('Invoking reactor handler', {
@@ -364,7 +366,7 @@ export class Reactors implements IReactors {
364
366
 
365
367
  lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
366
368
  } catch (err) {
367
- this._logger.error('Error handling event in reactor', { reactorId: id, error: String(err) });
369
+ this._logger.error('Error handling event in reactor', { reactorId: id, error: err });
368
370
  exceptionMessages.push(String(err));
369
371
  exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
370
372
  state = ObservationState.Failed;
@@ -386,9 +388,9 @@ export class Reactors implements IReactors {
386
388
  readModels: services.readModels, eventSequenceId, partition: eventsToObserve.Partition,
387
389
  signal: controller.signal, delivery: ArtifactDelivery.Events,
388
390
  eventContext: toClientEventContext(firstInvocableEvent.Context!)
389
- }, this._artifactActivator, processEvents);
391
+ }, this._artifactActivator, processEvents, this._connection.logger);
390
392
  } catch (err) {
391
- this._logger.error('Error activating reactor', { reactorId: id, error: String(err) });
393
+ this._logger.error('Error activating reactor', { reactorId: id, error: err });
392
394
  exceptionMessages.push(String(err));
393
395
  const completionStack = err instanceof Error ? (err.stack ?? '') : '';
394
396
  exceptionStackTrace = err instanceof ArtifactCompletionFailed && exceptionStackTrace
@@ -419,7 +421,7 @@ export class Reactors implements IReactors {
419
421
  if (!this._queues.has(id)) {
420
422
  this._logger.debug('Reactor observation stream closed cleanly', { reactorId: id });
421
423
  } else {
422
- this._logger.error('Reactor observation stream ended unexpectedly', { reactorId: id, error: String(err) });
424
+ this._logger.error('Reactor observation stream ended unexpectedly', { reactorId: id, error: err });
423
425
  }
424
426
  } finally {
425
427
  // Only retire our own queue: a reconnect can already have replaced it,