@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.
- package/ChronicleClient.ts +35 -33
- package/ChronicleOptions.ts +27 -1
- package/EventStore.ts +14 -12
- package/Metrics.ts +70 -96
- package/Tracing.ts +4 -2
- package/WellKnownTelemetryNames.ts +67 -0
- package/artifacts/validateArtifactSchemas.spec.ts +1 -1
- package/artifacts/withActivatedArtifact.ts +7 -5
- package/connection/ChronicleConnection.ts +18 -6
- package/connection/CompatibilityPreflight.ts +2 -1
- package/connection/KernelKeepAlive.ts +13 -7
- package/connection/TokenProvider.ts +3 -1
- package/connection/TraceContextMiddleware.ts +30 -0
- package/connection/for_ChronicleConnection/when_creating_clients/with_correlated_multi_host_callers.spec.ts +73 -0
- package/connection/for_OAuthTokenProvider/when_acquiring_tokens/with_http_instrumentation.spec.ts +102 -0
- package/connection/for_TraceContextMiddleware/when_calling/with_active_context.spec.ts +235 -0
- package/connection/for_TraceContextMiddleware/when_calling/with_grpc_instrumentation.spec.ts +102 -0
- package/correlation/CorrelationIdManager.ts +10 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +35 -33
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/ChronicleOptions.d.ts +8 -0
- package/dist/ChronicleOptions.d.ts.map +1 -1
- package/dist/ChronicleOptions.js +19 -1
- package/dist/ChronicleOptions.js.map +1 -1
- package/dist/EventStore.d.ts +3 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +14 -12
- package/dist/EventStore.js.map +1 -1
- package/dist/Metrics.d.ts +23 -64
- package/dist/Metrics.d.ts.map +1 -1
- package/dist/Metrics.js +62 -92
- package/dist/Metrics.js.map +1 -1
- package/dist/Tracing.d.ts +1 -1
- package/dist/Tracing.d.ts.map +1 -1
- package/dist/Tracing.js +4 -2
- package/dist/Tracing.js.map +1 -1
- package/dist/WellKnownTelemetryNames.d.ts +65 -0
- package/dist/WellKnownTelemetryNames.d.ts.map +1 -0
- package/dist/WellKnownTelemetryNames.js +67 -0
- package/dist/WellKnownTelemetryNames.js.map +1 -0
- package/dist/artifacts/withActivatedArtifact.d.ts +2 -1
- package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -1
- package/dist/artifacts/withActivatedArtifact.js +5 -4
- package/dist/artifacts/withActivatedArtifact.js.map +1 -1
- package/dist/connection/ChronicleConnection.d.ts +5 -0
- package/dist/connection/ChronicleConnection.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.js +13 -6
- package/dist/connection/ChronicleConnection.js.map +1 -1
- package/dist/connection/CompatibilityPreflight.d.ts.map +1 -1
- package/dist/connection/CompatibilityPreflight.js +2 -1
- package/dist/connection/CompatibilityPreflight.js.map +1 -1
- package/dist/connection/KernelKeepAlive.d.ts +3 -1
- package/dist/connection/KernelKeepAlive.d.ts.map +1 -1
- package/dist/connection/KernelKeepAlive.js +10 -7
- package/dist/connection/KernelKeepAlive.js.map +1 -1
- package/dist/connection/TokenProvider.d.ts.map +1 -1
- package/dist/connection/TokenProvider.js +3 -1
- package/dist/connection/TokenProvider.js.map +1 -1
- package/dist/connection/TraceContextMiddleware.d.ts +4 -0
- package/dist/connection/TraceContextMiddleware.d.ts.map +1 -0
- package/dist/connection/TraceContextMiddleware.js +30 -0
- package/dist/connection/TraceContextMiddleware.js.map +1 -0
- package/dist/correlation/CorrelationIdManager.d.ts +4 -0
- package/dist/correlation/CorrelationIdManager.d.ts.map +1 -1
- package/dist/correlation/CorrelationIdManager.js +8 -0
- package/dist/correlation/CorrelationIdManager.js.map +1 -1
- package/dist/eventSequences/EventLog.d.ts +2 -1
- package/dist/eventSequences/EventLog.d.ts.map +1 -1
- package/dist/eventSequences/EventLog.js +2 -2
- package/dist/eventSequences/EventLog.js.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts +4 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +63 -80
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.d.ts.map +1 -1
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js +3 -4
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/ChronicleLogEntry.d.ts +11 -0
- package/dist/logging/ChronicleLogEntry.d.ts.map +1 -0
- package/dist/logging/ChronicleLogEntry.js +4 -0
- package/dist/logging/ChronicleLogEntry.js.map +1 -0
- package/dist/logging/ChronicleLogLevel.d.ts +9 -0
- package/dist/logging/ChronicleLogLevel.d.ts.map +1 -0
- package/dist/logging/ChronicleLogLevel.js +12 -0
- package/dist/logging/ChronicleLogLevel.js.map +1 -0
- package/dist/logging/DiagChronicleLogger.d.ts +7 -0
- package/dist/logging/DiagChronicleLogger.d.ts.map +1 -0
- package/dist/logging/DiagChronicleLogger.js +10 -0
- package/dist/logging/DiagChronicleLogger.js.map +1 -0
- package/dist/logging/IChronicleLogger.d.ts +7 -0
- package/dist/logging/IChronicleLogger.d.ts.map +1 -0
- package/dist/logging/IChronicleLogger.js +4 -0
- package/dist/logging/IChronicleLogger.js.map +1 -0
- package/dist/logging/createLogger.d.ts +10 -0
- package/dist/logging/createLogger.d.ts.map +1 -0
- package/dist/logging/createLogger.js +55 -0
- package/dist/logging/createLogger.js.map +1 -0
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +4 -3
- package/dist/projections/Projections.js.map +1 -1
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +26 -24
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +28 -25
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/seeding/EventSeeding.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.js +4 -3
- package/dist/seeding/EventSeeding.js.map +1 -1
- package/dist/telemetry/ChronicleTelemetryOptions.d.ts +11 -0
- package/dist/telemetry/ChronicleTelemetryOptions.d.ts.map +1 -0
- package/dist/telemetry/ChronicleTelemetryOptions.js +4 -0
- package/dist/telemetry/ChronicleTelemetryOptions.js.map +1 -0
- package/dist/telemetry/observeOperation.d.ts +6 -0
- package/dist/telemetry/observeOperation.d.ts.map +1 -0
- package/dist/telemetry/observeOperation.js +23 -0
- package/dist/telemetry/observeOperation.js.map +1 -0
- package/dist/telemetry/runInBackgroundContext.d.ts +3 -0
- package/dist/telemetry/runInBackgroundContext.d.ts.map +1 -0
- package/dist/telemetry/runInBackgroundContext.js +9 -0
- package/dist/telemetry/runInBackgroundContext.js.map +1 -0
- package/dist/telemetry/spanAttributes.d.ts +14 -0
- package/dist/telemetry/spanAttributes.d.ts.map +1 -0
- package/dist/telemetry/spanAttributes.js +47 -0
- package/dist/telemetry/spanAttributes.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/EventLog.ts +4 -2
- package/eventSequences/EventSequence.ts +64 -81
- package/eventStoreSubscriptions/EventStoreSubscriptions.ts +5 -5
- package/for_ChronicleClient/when_starting_background_work/with_a_correlated_caller.spec.ts +150 -0
- package/for_ChronicleOptions/when_configuring_telemetry/with_event_source_identifiers.spec.ts +44 -0
- package/index.ts +7 -1
- package/logging/ChronicleLogEntry.ts +14 -0
- package/logging/ChronicleLogLevel.ts +11 -0
- package/logging/DiagChronicleLogger.ts +13 -0
- package/logging/IChronicleLogger.ts +10 -0
- package/logging/createLogger.ts +50 -0
- package/logging/for_ChronicleLogger/when_logging/with_client_owned_sinks.spec.ts +96 -0
- package/package.json +7 -1
- package/projections/Projections.ts +4 -3
- package/reactors/Reactors.ts +27 -25
- package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +14 -14
- package/reducers/Reducers.ts +29 -26
- package/seeding/EventSeeding.ts +6 -4
- package/telemetry/ChronicleTelemetryOptions.ts +8 -0
- package/telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.ts +36 -0
- package/telemetry/for_ChronicleTelemetry/given/an_event_sequence.fixture.ts +29 -0
- package/telemetry/for_ChronicleTelemetry/when_appending/with_shared_and_legacy_attributes.spec.ts +58 -0
- package/telemetry/for_ChronicleTelemetry/when_appending/without_an_sdk.spec.ts +15 -0
- package/telemetry/for_ChronicleTelemetry/when_documenting/with_public_names.spec.ts +15 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_failures/with_sensitive_exception_details.spec.ts +34 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts +92 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_legacy_overlap.spec.ts +40 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_sequence_numbers/with_integer_precision.spec.ts +23 -0
- package/telemetry/observeOperation.ts +25 -0
- package/telemetry/runInBackgroundContext.ts +10 -0
- 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.
|
|
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 {
|
|
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
|
|
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
|
|
300
|
+
error
|
|
300
301
|
});
|
|
301
302
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
302
303
|
delay = Math.min(delay * 2, 15000);
|
package/reactors/Reactors.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|