@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
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import { EventContext as WireContext, EventObservationState, EventType, ObservationState, type ReactorMessage } from '@cratis/chronicle.contracts';
|
|
5
5
|
import { ReplayState } from '../../../index.js';
|
|
6
6
|
import { chai, describe, it, vi } from 'vitest';
|
|
7
|
-
import {
|
|
7
|
+
import type { IChronicleLogger } from '../../../logging/IChronicleLogger.js';
|
|
8
|
+
import type { ChronicleLogEntry } from '../../../logging/ChronicleLogEntry.js';
|
|
8
9
|
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
9
10
|
import { ConnectionLifecycle } from '../../../connection/ConnectionLifecycle.js';
|
|
10
11
|
import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifactsProvider.js';
|
|
@@ -98,7 +99,7 @@ function delivery(events: ReturnType<typeof event>[], replayState = ReplayState.
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
async function observe(type: Function, batches: ReturnType<typeof delivery>[], namespace = 'tenant-a',
|
|
101
|
-
activator?: ClientArtifactsActivator, appendResult = true, onAppendMany?: () => void) {
|
|
102
|
+
activator?: ClientArtifactsActivator, appendResult = true, onAppendMany?: () => void, logger?: IChronicleLogger) {
|
|
102
103
|
const acknowledgements: NonNullable<ReactorMessage['Content']>['Value1'][] = [];
|
|
103
104
|
const append = vi.fn().mockResolvedValue({ isSuccess: appendResult });
|
|
104
105
|
const appendMany = vi.fn().mockImplementation(async () => {
|
|
@@ -122,7 +123,7 @@ async function observe(type: Function, batches: ReturnType<typeof delivery>[], n
|
|
|
122
123
|
await lifecycle.disconnected(error => { throw error; });
|
|
123
124
|
finished();
|
|
124
125
|
}
|
|
125
|
-
const connection = { reactors: { observe: stream } } as unknown as ChronicleConnection;
|
|
126
|
+
const connection = { logger, reactors: { observe: stream } } as unknown as ChronicleConnection;
|
|
126
127
|
const artifacts = { reactors: [type], eventTypes: [ActivationEvent] } as unknown as IClientArtifactsProvider;
|
|
127
128
|
await new Reactors(artifacts, connection, 'store', namespace, lifecycle, eventLog, undefined, store, activator).register();
|
|
128
129
|
await complete;
|
|
@@ -462,16 +463,15 @@ describe('when delivering reactor batches', () => {
|
|
|
462
463
|
});
|
|
463
464
|
|
|
464
465
|
it('should log disposal failure without changing successful acknowledgement', async () => {
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
466
|
+
const entries: ChronicleLogEntry[] = [];
|
|
467
|
+
const activator: ClientArtifactsActivator = type => ({ instance: new type(), dispose: async () => { throw new Error('private cleanup details'); } });
|
|
468
|
+
const result = await observe(ActivationReactor, [delivery([event('private payload', 1n)])], 'tenant-a', activator,
|
|
469
|
+
true, undefined, { log: entry => entries.push(entry) });
|
|
470
|
+
result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
|
|
471
|
+
result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(1n);
|
|
472
|
+
entries.filter(entry => entry.attributes['exception.type'] === 'Error').should.have.lengthOf(1);
|
|
473
|
+
JSON.stringify(entries).should.not.contain('private cleanup details');
|
|
474
|
+
JSON.stringify(entries).should.not.contain('private payload');
|
|
475
|
+
JSON.stringify(entries).should.not.contain('book-1');
|
|
476
476
|
});
|
|
477
477
|
});
|
package/reducers/Reducers.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, ReadModelObserverType, ReducerMessage, ReplayState } from '@cratis/chronicle.contracts';
|
|
8
9
|
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
@@ -102,7 +103,7 @@ export class Reducers implements IReducers {
|
|
|
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 _reducers = new Map<string, Constructor>();
|
|
108
109
|
private readonly _queues = new Map<string, AsyncQueue<ReducerMessage>>();
|
|
@@ -133,6 +134,7 @@ export class Reducers implements IReducers {
|
|
|
133
134
|
private readonly _artifactActivator?: ClientArtifactsActivator,
|
|
134
135
|
private readonly _readModelNamingPolicy?: ReadModelNamingPolicy
|
|
135
136
|
) {
|
|
137
|
+
this._logger = createLogger('@cratis/chronicle/reducers', _connection.logger);
|
|
136
138
|
this._lifecycle = lifecycle;
|
|
137
139
|
lifecycle.onDisconnected(async () => {
|
|
138
140
|
this._logger.info('Disconnected — stopping all reducer observations');
|
|
@@ -235,21 +237,23 @@ export class Reducers implements IReducers {
|
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
private startObservation(id: string, reducerType: Constructor): void {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
this._logger.info('Starting reducer observation', {
|
|
245
|
-
reducerId: id,
|
|
246
|
-
eventSequenceId,
|
|
247
|
-
readModel: readModelName,
|
|
248
|
-
handlerCount: dispatcher.handlers.length,
|
|
249
|
-
handlers: dispatcher.handlers.map(e => e.methodName)
|
|
250
|
-
});
|
|
240
|
+
runInBackgroundContext(() => {
|
|
241
|
+
if (this._disposed) return;
|
|
242
|
+
const metadata = getReducerMetadata(reducerType)!;
|
|
243
|
+
const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
|
|
244
|
+
const dispatcher = new ReducerEventDispatcher(reducerType, this._clientArtifacts.eventTypes);
|
|
245
|
+
const readModelName = this.getReducerReadModelIdentifier(reducerType);
|
|
251
246
|
|
|
252
|
-
|
|
247
|
+
this._logger.info('Starting reducer observation', {
|
|
248
|
+
reducerId: id,
|
|
249
|
+
eventSequenceId,
|
|
250
|
+
readModel: readModelName,
|
|
251
|
+
handlerCount: dispatcher.handlers.length,
|
|
252
|
+
handlers: dispatcher.handlers.map(e => e.methodName)
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
void this.runObservation(id, reducerType, eventSequenceId, readModelName, dispatcher);
|
|
256
|
+
});
|
|
253
257
|
}
|
|
254
258
|
|
|
255
259
|
private async runObservation(
|
|
@@ -262,7 +266,7 @@ export class Reducers implements IReducers {
|
|
|
262
266
|
try {
|
|
263
267
|
await this.observeReducer(id, reducerType, eventSequenceId, readModelName, dispatcher);
|
|
264
268
|
} catch (error) {
|
|
265
|
-
this._logger.error('Reducer observation loop exited with error', { reducerId: id, error
|
|
269
|
+
this._logger.error('Reducer observation loop exited with error', { reducerId: id, error });
|
|
266
270
|
}
|
|
267
271
|
|
|
268
272
|
this.scheduleReobserve(id, reducerType);
|
|
@@ -285,14 +289,14 @@ export class Reducers implements IReducers {
|
|
|
285
289
|
return;
|
|
286
290
|
}
|
|
287
291
|
|
|
288
|
-
const handle = setTimeout(() => {
|
|
292
|
+
const handle = runInBackgroundContext(() => setTimeout(() => {
|
|
289
293
|
if (!this._registered || this._disposed) {
|
|
290
294
|
return;
|
|
291
295
|
}
|
|
292
296
|
|
|
293
297
|
this._logger.info('Re-establishing reducer observation', { reducerId: id });
|
|
294
298
|
this.startObservation(id, reducerType);
|
|
295
|
-
}, Reducers._reobserveDelayMs);
|
|
299
|
+
}, Reducers._reobserveDelayMs));
|
|
296
300
|
|
|
297
301
|
handle.unref?.();
|
|
298
302
|
}
|
|
@@ -359,7 +363,6 @@ export class Reducers implements IReducers {
|
|
|
359
363
|
|
|
360
364
|
this._logger.debug('Received reduce operation', {
|
|
361
365
|
reducerId: id,
|
|
362
|
-
partition: operation.Partition,
|
|
363
366
|
count: operation.Events.length,
|
|
364
367
|
hasInitialState: operation.InitialState !== '',
|
|
365
368
|
replayState: operation.ReplayState
|
|
@@ -375,12 +378,12 @@ export class Reducers implements IReducers {
|
|
|
375
378
|
replayState: operation.ReplayState
|
|
376
379
|
}, this._artifactActivator, artifact => runActivated(artifact, () =>
|
|
377
380
|
notifyReplayLifecycle(artifact.instance, operation.ReplayState, operation.Partition),
|
|
378
|
-
{ delivery: ArtifactDelivery.ReplayNotification, replayState: operation.ReplayState }));
|
|
381
|
+
{ delivery: ArtifactDelivery.ReplayNotification, replayState: operation.ReplayState }), this._connection.logger);
|
|
379
382
|
} else if (reducerInstance) {
|
|
380
383
|
await notifyReplayLifecycle(reducerInstance, operation.ReplayState, operation.Partition);
|
|
381
384
|
}
|
|
382
385
|
} catch (err) {
|
|
383
|
-
this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: id, error:
|
|
386
|
+
this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: id, error: err });
|
|
384
387
|
if (err instanceof ArtifactCompletionFailed && err.processingError !== undefined) {
|
|
385
388
|
exceptionMessages.push(String(err.processingError));
|
|
386
389
|
}
|
|
@@ -424,7 +427,7 @@ export class Reducers implements IReducers {
|
|
|
424
427
|
{ delivery: ArtifactDelivery.Events, eventContext: context, methodName: entry.methodName });
|
|
425
428
|
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
426
429
|
} catch (err) {
|
|
427
|
-
this._logger.error('Error handling event in reducer', { reducerId: id, error:
|
|
430
|
+
this._logger.error('Error handling event in reducer', { reducerId: id, error: err });
|
|
428
431
|
exceptionMessages.push(String(err));
|
|
429
432
|
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
430
433
|
state = ObservationState.Failed;
|
|
@@ -445,9 +448,9 @@ export class Reducers implements IReducers {
|
|
|
445
448
|
readModels: this._eventStore.readModels, eventSequenceId, partition: operation.Partition,
|
|
446
449
|
signal: controller.signal, delivery: ArtifactDelivery.Events,
|
|
447
450
|
eventContext: toClientEventContext(firstInvocableEvent.Context!)
|
|
448
|
-
}, this._artifactActivator, processEvents);
|
|
451
|
+
}, this._artifactActivator, processEvents, this._connection.logger);
|
|
449
452
|
} catch (err) {
|
|
450
|
-
this._logger.error('Error activating reducer', { reducerId: id, error:
|
|
453
|
+
this._logger.error('Error activating reducer', { reducerId: id, error: err });
|
|
451
454
|
exceptionMessages.push(String(err));
|
|
452
455
|
const completionStack = err instanceof Error ? (err.stack ?? '') : '';
|
|
453
456
|
exceptionStackTrace = err instanceof ArtifactCompletionFailed && exceptionStackTrace
|
|
@@ -483,7 +486,7 @@ export class Reducers implements IReducers {
|
|
|
483
486
|
if (!this._queues.has(id)) {
|
|
484
487
|
this._logger.debug('Reducer observation stream closed cleanly', { reducerId: id });
|
|
485
488
|
} else {
|
|
486
|
-
this._logger.error('Reducer observation stream ended unexpectedly', { reducerId: id, error:
|
|
489
|
+
this._logger.error('Reducer observation stream ended unexpectedly', { reducerId: id, error: err });
|
|
487
490
|
}
|
|
488
491
|
} finally {
|
|
489
492
|
// Only retire our own queue: a reconnect can already have replaced it,
|
package/seeding/EventSeeding.ts
CHANGED
|
@@ -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 { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
6
6
|
import { ChronicleConnection } from '../connection/index.js';
|
|
7
7
|
import { ensureCommandSuccess } from '../connection/callResults.js';
|
|
@@ -27,7 +27,7 @@ interface SeedingEntry {
|
|
|
27
27
|
* with the Chronicle Kernel.
|
|
28
28
|
*/
|
|
29
29
|
export class EventSeeding implements IEventSeeding {
|
|
30
|
-
private readonly _logger
|
|
30
|
+
private readonly _logger: ReturnType<typeof createLogger>;
|
|
31
31
|
private readonly _entries: SeedingEntry[] = [];
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -40,7 +40,9 @@ export class EventSeeding implements IEventSeeding {
|
|
|
40
40
|
private readonly _eventStoreName: string,
|
|
41
41
|
private readonly _connection: ChronicleConnection,
|
|
42
42
|
private readonly _clientArtifacts: IClientArtifactsProvider
|
|
43
|
-
) {
|
|
43
|
+
) {
|
|
44
|
+
this._logger = createLogger('@cratis/chronicle/EventSeeding', _connection.logger);
|
|
45
|
+
}
|
|
44
46
|
|
|
45
47
|
/** @inheritdoc */
|
|
46
48
|
for<TEvent extends object>(eventSourceId: string, events: Iterable<TEvent>): IEventSeedingBuilder {
|
|
@@ -69,7 +71,7 @@ export class EventSeeding implements IEventSeeding {
|
|
|
69
71
|
} catch (error) {
|
|
70
72
|
this._logger.warn('Failed to activate or execute event seeder - skipping seeder', {
|
|
71
73
|
seederType: seederType.name,
|
|
72
|
-
error
|
|
74
|
+
error
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
77
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
/** Per-client privacy settings; instrumentation itself is always available. */
|
|
5
|
+
export interface ChronicleTelemetryOptions {
|
|
6
|
+
/** Absent means no event source identifiers. HMAC uses SHA-256 and a deployment-owned key. */
|
|
7
|
+
eventSourceId?: { mode: 'raw' } | { mode: 'hmac'; key: Uint8Array };
|
|
8
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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 { beforeAll, beforeEach, afterAll } from 'vitest';
|
|
5
|
+
import { context, metrics, propagation, trace } from '@opentelemetry/api';
|
|
6
|
+
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
|
|
7
|
+
import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator } from '@opentelemetry/core';
|
|
8
|
+
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
9
|
+
import { InMemoryMetricExporter, MeterProvider, PeriodicExportingMetricReader, AggregationTemporality } from '@opentelemetry/sdk-metrics';
|
|
10
|
+
|
|
11
|
+
/** In-process providers only; no export outside the spec process. */
|
|
12
|
+
export function telemetrySession() {
|
|
13
|
+
const spans = new InMemorySpanExporter();
|
|
14
|
+
const traces = new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(spans)] });
|
|
15
|
+
const metricExporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE);
|
|
16
|
+
const reader = new PeriodicExportingMetricReader({ exporter: metricExporter, exportIntervalMillis: 60000 });
|
|
17
|
+
const meters = new MeterProvider({ readers: [reader] });
|
|
18
|
+
const contextManager = new AsyncLocalStorageContextManager();
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
context.setGlobalContextManager(contextManager.enable());
|
|
21
|
+
trace.setGlobalTracerProvider(traces);
|
|
22
|
+
metrics.setGlobalMeterProvider(meters);
|
|
23
|
+
propagation.setGlobalPropagator(new CompositePropagator({ propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()] }));
|
|
24
|
+
});
|
|
25
|
+
beforeEach(() => spans.reset());
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
await traces.shutdown();
|
|
28
|
+
await meters.shutdown();
|
|
29
|
+
context.disable();
|
|
30
|
+
trace.disable();
|
|
31
|
+
metrics.disable();
|
|
32
|
+
propagation.disable();
|
|
33
|
+
contextManager.disable();
|
|
34
|
+
});
|
|
35
|
+
return { spans, traces, reader };
|
|
36
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 { vi } from 'vitest';
|
|
5
|
+
import { EventSequence } from '../../../eventSequences/EventSequence.js';
|
|
6
|
+
import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
|
|
7
|
+
import { eventType } from '../../../events/eventTypeDecorator.js';
|
|
8
|
+
import type { ChronicleConnection } from '../../../connection/ChronicleConnection.js';
|
|
9
|
+
import type { IUnitOfWorkManager } from '../../../transactions/IUnitOfWorkManager.js';
|
|
10
|
+
import type { ChronicleTelemetryOptions } from '../../ChronicleTelemetryOptions.js';
|
|
11
|
+
|
|
12
|
+
export class Recorded { constructor(readonly value = 'sensitive payload') {} }
|
|
13
|
+
eventType('telemetry-recorded')(Recorded);
|
|
14
|
+
|
|
15
|
+
export function eventSequence(telemetry?: ChronicleTelemetryOptions) {
|
|
16
|
+
const services = {
|
|
17
|
+
append: vi.fn().mockResolvedValue({ Response: { SequenceNumber: 42n } }),
|
|
18
|
+
appendManyForEventSources: vi.fn().mockResolvedValue({ Response: { SequenceNumbers: [42n, 43n] } }),
|
|
19
|
+
tailSequenceNumber: vi.fn().mockResolvedValue({ IsAuthorized: true, Data: { SequenceNumber: 42n } }),
|
|
20
|
+
hasEventsForEventSourceId: vi.fn().mockResolvedValue({ IsAuthorized: true, Data: { HasEvents: true } }),
|
|
21
|
+
forEventSourceIdAndEventTypes: vi.fn().mockResolvedValue({ IsAuthorized: true, Data: [] }),
|
|
22
|
+
fromSequenceNumber: vi.fn().mockResolvedValue({ IsAuthorized: true, Data: [] }),
|
|
23
|
+
redact: vi.fn().mockResolvedValue({}),
|
|
24
|
+
redactForEventSource: vi.fn().mockResolvedValue({})
|
|
25
|
+
};
|
|
26
|
+
const connection = { eventSequences: services } as unknown as ChronicleConnection;
|
|
27
|
+
return { services, sequence: new EventSequence(EventSequenceId.eventLog, 'store', 'namespace', connection,
|
|
28
|
+
{} as IUnitOfWorkManager, undefined, telemetry) };
|
|
29
|
+
}
|
package/telemetry/for_ChronicleTelemetry/when_appending/with_shared_and_legacy_attributes.spec.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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 } from 'vitest';
|
|
5
|
+
import { SpanKind, context, propagation } from '@opentelemetry/api';
|
|
6
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
7
|
+
import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
|
|
8
|
+
import { eventSequence, Recorded } from '../given/an_event_sequence.fixture.js';
|
|
9
|
+
import { clientVersion } from '../../../connection/clientVersion.js';
|
|
10
|
+
import { CorrelationId, correlationIdManager } from '../../../correlation/index.js';
|
|
11
|
+
|
|
12
|
+
const should = chai.should();
|
|
13
|
+
const telemetry = telemetrySession();
|
|
14
|
+
const correlation = '95bd1c83-de97-45f7-84a0-fb462af14d0c';
|
|
15
|
+
|
|
16
|
+
describe('when appending with a business correlation override', () => {
|
|
17
|
+
let span: ReadableSpan;
|
|
18
|
+
let injectedCorrelation: string | undefined;
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
const { sequence, services } = eventSequence();
|
|
21
|
+
services.append.mockImplementation(async () => {
|
|
22
|
+
await Promise.resolve();
|
|
23
|
+
injectedCorrelation = propagation.getBaggage(context.active())?.getEntry('cratis.correlation_id')?.value;
|
|
24
|
+
return { Response: { SequenceNumber: 42n } };
|
|
25
|
+
});
|
|
26
|
+
await correlationIdManager.run(new CorrelationId('outer'), () => sequence.append('sensitive-id', new Recorded(), { correlationId: correlation }));
|
|
27
|
+
span = telemetry.spans.getFinishedSpans()[0];
|
|
28
|
+
});
|
|
29
|
+
it('should produce one client span with the unchanged operation name', () => {
|
|
30
|
+
telemetry.spans.getFinishedSpans().should.have.lengthOf(1);
|
|
31
|
+
span.kind.should.equal(SpanKind.CLIENT);
|
|
32
|
+
span.name.should.equal('chronicle.event_sequences.append');
|
|
33
|
+
});
|
|
34
|
+
it('should use the versioned shared instrumentation scope', () => {
|
|
35
|
+
span.instrumentationScope.name.should.equal('Cratis.Chronicle.Client');
|
|
36
|
+
should.equal(span.instrumentationScope.version, clientVersion);
|
|
37
|
+
});
|
|
38
|
+
it('should emit both registries on the same span', () => {
|
|
39
|
+
span.attributes.should.include({
|
|
40
|
+
'cratis.event_store.name': 'store', 'chronicle.event_store': 'store',
|
|
41
|
+
'cratis.event_store.namespace': 'namespace', 'chronicle.namespace': 'namespace',
|
|
42
|
+
'cratis.event_sequence.id': 'event-log', 'chronicle.event_sequence_id': 'event-log',
|
|
43
|
+
'cratis.event_type.id': 'telemetry-recorded', 'chronicle.event_type_id': 'telemetry-recorded',
|
|
44
|
+
'cratis.event_type.generation': 1, 'chronicle.event_type_generation': 1,
|
|
45
|
+
'cratis.event_sequence.number': 42, 'chronicle.sequence_number': '42'
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('should scope the resolved correlation consistently for propagation', () => {
|
|
49
|
+
should.equal(span.attributes['cratis.correlation_id'], correlation);
|
|
50
|
+
should.equal(injectedCorrelation, correlation);
|
|
51
|
+
should.equal(correlationIdManager.scoped, undefined);
|
|
52
|
+
});
|
|
53
|
+
it('should not record identifiers or payloads by default', () => {
|
|
54
|
+
JSON.stringify(span.attributes).should.not.contain('sensitive');
|
|
55
|
+
should.equal(span.attributes['cratis.event_source.id'], undefined);
|
|
56
|
+
should.equal(span.attributes['chronicle.event_source_id'], undefined);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
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 { eventSequence, Recorded } from '../given/an_event_sequence.fixture.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
describe('when appending without an OpenTelemetry SDK', () => {
|
|
9
|
+
it('should preserve append behavior without telemetry setup', async () => {
|
|
10
|
+
const { sequence, services } = eventSequence();
|
|
11
|
+
const result = await sequence.append('source', new Recorded());
|
|
12
|
+
result.sequenceNumber.value.should.equal(42n);
|
|
13
|
+
services.append.mock.calls.should.have.lengthOf(1);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
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 { readFileSync } from 'node:fs';
|
|
5
|
+
import { chai, describe, it } from 'vitest';
|
|
6
|
+
import { WellKnownTelemetryNames } from '../../../WellKnownTelemetryNames.js';
|
|
7
|
+
import { telemetryReference } from '../../../scripts/telemetry-reference.mjs';
|
|
8
|
+
|
|
9
|
+
chai.should();
|
|
10
|
+
describe('when publishing the telemetry reference', () => {
|
|
11
|
+
it('should keep the documented registry identical to the public constants', () => {
|
|
12
|
+
const documentation = readFileSync(new URL('../../../../Documentation/observability.md', import.meta.url), 'utf8');
|
|
13
|
+
documentation.should.contain(telemetryReference(WellKnownTelemetryNames));
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
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 } from 'vitest';
|
|
5
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
6
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
7
|
+
import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
|
|
8
|
+
import { eventSequence, Recorded } from '../given/an_event_sequence.fixture.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
const telemetry = telemetrySession();
|
|
12
|
+
|
|
13
|
+
describe('when an append fails with sensitive exception details', () => {
|
|
14
|
+
const failure = new TypeError('sensitive token in error message');
|
|
15
|
+
let span: ReadableSpan;
|
|
16
|
+
let thrown: unknown;
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
const { sequence, services } = eventSequence();
|
|
19
|
+
services.append.mockRejectedValue(failure);
|
|
20
|
+
try { await sequence.append('private', new Recorded()); } catch (error) { thrown = error; }
|
|
21
|
+
span = telemetry.spans.getFinishedSpans()[0];
|
|
22
|
+
});
|
|
23
|
+
it('should rethrow the original failure', () => { (thrown === failure).should.be.true; });
|
|
24
|
+
it('should record only failure types and error status', () => {
|
|
25
|
+
span.status.should.deep.equal({ code: SpanStatusCode.ERROR });
|
|
26
|
+
span.attributes.should.include({ 'error.type': 'TypeError' });
|
|
27
|
+
span.events.should.have.lengthOf(1);
|
|
28
|
+
span.events[0].attributes!.should.deep.equal({ 'exception.type': 'TypeError' });
|
|
29
|
+
});
|
|
30
|
+
it('should never export the exception message or stack', () => {
|
|
31
|
+
JSON.stringify({ attributes: span.attributes, status: span.status, events: span.events }).should.not.contain('sensitive');
|
|
32
|
+
span.events[0].attributes!.should.not.have.property('exception.stacktrace');
|
|
33
|
+
});
|
|
34
|
+
});
|
package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
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 { createHmac } from 'node:crypto';
|
|
5
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
6
|
+
import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
|
|
7
|
+
import { eventSequence, Recorded } from '../given/an_event_sequence.fixture.js';
|
|
8
|
+
import { EventSequenceNumber } from '../../../eventSequences/EventSequenceNumber.js';
|
|
9
|
+
import { ChronicleOptions } from '../../../ChronicleOptions.js';
|
|
10
|
+
import type { ChronicleTelemetryOptions } from '../../ChronicleTelemetryOptions.js';
|
|
11
|
+
|
|
12
|
+
const should = chai.should();
|
|
13
|
+
const telemetry = telemetrySession();
|
|
14
|
+
const source = 'sensitive-source';
|
|
15
|
+
const key = new Uint8Array([10, 20, 30, 40]);
|
|
16
|
+
|
|
17
|
+
for (const [label, policy, expected] of [
|
|
18
|
+
['absent', undefined, undefined],
|
|
19
|
+
['raw', { eventSourceId: { mode: 'raw' } }, source],
|
|
20
|
+
['hmac', { eventSourceId: { mode: 'hmac', key } }, createHmac('sha256', key).update(source).digest('hex')]
|
|
21
|
+
] as const) {
|
|
22
|
+
describe(`when recording event source identifiers with ${label} privacy`, () => {
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
const { sequence, services } = eventSequence(policy as ChronicleTelemetryOptions | undefined);
|
|
25
|
+
await sequence.append(source, new Recorded());
|
|
26
|
+
await sequence.appendMany(source, [new Recorded(), new Recorded()]);
|
|
27
|
+
await sequence.appendMany([{ eventSourceId: source, event: new Recorded() }, { eventSourceId: source, event: new Recorded() }]);
|
|
28
|
+
await sequence.getTailSequenceNumber(source);
|
|
29
|
+
await sequence.hasEventsFor(source);
|
|
30
|
+
await sequence.getForEventSourceIdAndEventTypes(source, [Recorded]);
|
|
31
|
+
await sequence.getFromSequenceNumber(new EventSequenceNumber(1n), source);
|
|
32
|
+
await sequence.redactForEventSource(source, 'private reason');
|
|
33
|
+
services.append.mock.calls[0][0].EventSourceId.should.equal(source);
|
|
34
|
+
});
|
|
35
|
+
it('should apply the policy equally to both names on every operation', () => {
|
|
36
|
+
telemetry.spans.getFinishedSpans().should.have.lengthOf(8);
|
|
37
|
+
for (const span of telemetry.spans.getFinishedSpans()) {
|
|
38
|
+
should.equal(span.attributes['cratis.event_source.id'], expected);
|
|
39
|
+
should.equal(span.attributes['chronicle.event_source_id'], expected);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('when a validated telemetry policy is later mutated by an untyped caller', () => {
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
const options = ChronicleOptions.development({ telemetry: { eventSourceId: { mode: 'hmac', key } } });
|
|
48
|
+
Object.assign(options.telemetry!.eventSourceId!, { key: undefined });
|
|
49
|
+
const { sequence, services } = eventSequence(options.telemetry);
|
|
50
|
+
await sequence.append(source, new Recorded());
|
|
51
|
+
await sequence.getTailSequenceNumber(source);
|
|
52
|
+
await sequence.redactForEventSource(source, 'reason');
|
|
53
|
+
services.append.mock.calls.should.have.lengthOf(1);
|
|
54
|
+
services.tailSequenceNumber.mock.calls.should.have.lengthOf(1);
|
|
55
|
+
services.redactForEventSource.mock.calls.should.have.lengthOf(1);
|
|
56
|
+
});
|
|
57
|
+
it('should omit the failed enrichment without failing business calls or leaving spans open', () => {
|
|
58
|
+
telemetry.spans.getFinishedSpans().should.have.lengthOf(3);
|
|
59
|
+
for (const span of telemetry.spans.getFinishedSpans()) {
|
|
60
|
+
span.attributes.should.not.have.property('cratis.event_source.id');
|
|
61
|
+
span.attributes.should.not.have.property('chronicle.event_source_id');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('when appending a batch with multiple event sources', () => {
|
|
67
|
+
beforeEach(async () => {
|
|
68
|
+
await eventSequence({ eventSourceId: { mode: 'raw' } }).sequence.appendMany([
|
|
69
|
+
{ eventSourceId: 'first', event: new Recorded() }, { eventSourceId: 'second', event: new Recorded() }
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
it('should record the count without an identifier array', () => {
|
|
73
|
+
const attributes = telemetry.spans.getFinishedSpans()[0].attributes;
|
|
74
|
+
attributes.should.include({ 'cratis.event.count': 2, 'chronicle.events_count': 2 });
|
|
75
|
+
should.equal(attributes['cratis.event_source.id'], undefined);
|
|
76
|
+
should.equal(attributes['chronicle.event_source_id'], undefined);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('when clients use different identifier policies concurrently', () => {
|
|
81
|
+
beforeEach(async () => {
|
|
82
|
+
await Promise.all([
|
|
83
|
+
eventSequence().sequence.append(source, new Recorded()),
|
|
84
|
+
eventSequence({ eventSourceId: { mode: 'raw' } }).sequence.append(source, new Recorded()),
|
|
85
|
+
eventSequence({ eventSourceId: { mode: 'hmac', key } }).sequence.append(source, new Recorded())
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
it('should keep each policy local to its client', () => {
|
|
89
|
+
const identifiers = telemetry.spans.getFinishedSpans().map(span => span.attributes['cratis.event_source.id']);
|
|
90
|
+
identifiers.should.have.members([undefined, source, createHmac('sha256', key).update(source).digest('hex')]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
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 } from 'vitest';
|
|
5
|
+
import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
|
|
6
|
+
import { ChronicleMetrics } from '../../../Metrics.js';
|
|
7
|
+
import { clientVersion } from '../../../connection/clientVersion.js';
|
|
8
|
+
import type { ScopeMetrics } from '@opentelemetry/sdk-metrics';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
const telemetry = telemetrySession();
|
|
12
|
+
describe('when recording metrics after installing the SDK', () => {
|
|
13
|
+
let scope: ScopeMetrics;
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
// Metrics was imported before provider installation: recording must still reach the SDK.
|
|
16
|
+
const attributes = { 'chronicle.event_store': 'store', 'chronicle.namespace': 'namespace',
|
|
17
|
+
'chronicle.event_sequence_id': 'event-log', 'chronicle.events_count': 237 };
|
|
18
|
+
ChronicleMetrics.batchAppendsPerformed.add(1, attributes);
|
|
19
|
+
ChronicleMetrics.appendManyDuration.record(250, attributes);
|
|
20
|
+
scope = (await telemetry.reader.collect()).resourceMetrics.scopeMetrics[0];
|
|
21
|
+
});
|
|
22
|
+
it('should use the shared scope and installed version', () => {
|
|
23
|
+
scope.scope.should.include({ name: 'Cratis.Chronicle.Client', version: clientVersion });
|
|
24
|
+
});
|
|
25
|
+
it('should retain legacy milliseconds while emitting seconds', () => {
|
|
26
|
+
const legacy = scope.metrics.find(metric => metric.descriptor.name === 'chronicle.events.append_many_duration')!;
|
|
27
|
+
const shared = scope.metrics.find(metric => metric.descriptor.name === 'cratis.chronicle.event_sequence.append_many_duration')!;
|
|
28
|
+
legacy.descriptor.unit.should.equal('ms');
|
|
29
|
+
shared.descriptor.unit.should.equal('s');
|
|
30
|
+
legacy.dataPoints[0].value.should.include({ min: 250, max: 250 });
|
|
31
|
+
shared.dataPoints[0].value.should.include({ min: 0.25, max: 0.25 });
|
|
32
|
+
});
|
|
33
|
+
it('should exclude batch size from only the new metric dimensions', () => {
|
|
34
|
+
const legacy = scope.metrics.find(metric => metric.descriptor.name === 'chronicle.events.batch_appends')!;
|
|
35
|
+
const shared = scope.metrics.find(metric => metric.descriptor.name === 'cratis.chronicle.event_sequence.batch_appends')!;
|
|
36
|
+
legacy.dataPoints[0].attributes.should.have.property('chronicle.events_count', 237);
|
|
37
|
+
shared.dataPoints[0].attributes.should.deep.equal({ 'cratis.event_store.name': 'store',
|
|
38
|
+
'cratis.event_store.namespace': 'namespace', 'cratis.event_sequence.id': 'event-log' });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
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 } from 'vitest';
|
|
5
|
+
import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
|
|
6
|
+
import { eventSequence } from '../given/an_event_sequence.fixture.js';
|
|
7
|
+
|
|
8
|
+
const should = chai.should();
|
|
9
|
+
const telemetry = telemetrySession();
|
|
10
|
+
for (const [number, expected] of [[42n, 42], [9007199254740991n, Number.MAX_SAFE_INTEGER], [9007199254740992n, undefined], [18446744073709551615n, undefined]] as const) {
|
|
11
|
+
describe(`when reading sequence number ${number}`, () => {
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
const { sequence, services } = eventSequence();
|
|
14
|
+
services.tailSequenceNumber.mockResolvedValue({ IsAuthorized: true, Data: { SequenceNumber: number } });
|
|
15
|
+
await sequence.getTailSequenceNumber();
|
|
16
|
+
});
|
|
17
|
+
it('should keep the legacy string and emit only exactly representable shared integers', () => {
|
|
18
|
+
const attributes = telemetry.spans.getFinishedSpans()[0].attributes;
|
|
19
|
+
should.equal(attributes['cratis.event_sequence.number'], expected);
|
|
20
|
+
should.equal(attributes['chronicle.sequence_number'], number.toString());
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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, SpanKind, type Span } from '@opentelemetry/api';
|
|
5
|
+
import { ChronicleTracer } from '../Tracing.js';
|
|
6
|
+
import { correlationIdManager } from '../correlation/index.js';
|
|
7
|
+
import { CorrelationId } from '../correlation/CorrelationId.js';
|
|
8
|
+
import { WellKnownTelemetryNames } from '../WellKnownTelemetryNames.js';
|
|
9
|
+
|
|
10
|
+
/** Opens one client span, scoping the resolved business correlation without generating a new one. */
|
|
11
|
+
export function observeOperation<T>(name: string, action: (span: Span) => Promise<T>, correlationId?: { toString(): string }): Promise<T> {
|
|
12
|
+
const resolved = correlationId ? new CorrelationId(correlationId.toString()) : correlationIdManager.scoped;
|
|
13
|
+
const run = () => {
|
|
14
|
+
const active = context.active();
|
|
15
|
+
const key = WellKnownTelemetryNames.attributes.correlationId;
|
|
16
|
+
const correlation = resolved?.toString() ?? propagation.getBaggage(active)?.getEntry(key)?.value;
|
|
17
|
+
const parent = correlation ? propagation.setBaggage(active,
|
|
18
|
+
(propagation.getBaggage(active) ?? propagation.createBaggage()).setEntry(key, { value: correlation })) : active;
|
|
19
|
+
return ChronicleTracer.startActiveSpan(name, {
|
|
20
|
+
kind: SpanKind.CLIENT,
|
|
21
|
+
attributes: correlation ? { [key]: correlation } : {}
|
|
22
|
+
}, parent, action);
|
|
23
|
+
};
|
|
24
|
+
return resolved ? correlationIdManager.run(resolved, run) : run();
|
|
25
|
+
}
|
|
@@ -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 { context, ROOT_CONTEXT } from '@opentelemetry/api';
|
|
5
|
+
import { correlationIdManager } from '../correlation/index.js';
|
|
6
|
+
|
|
7
|
+
/** Starts client-owned work without retaining the initiating request's trace or business correlation. */
|
|
8
|
+
export function runInBackgroundContext<T>(action: () => T): T {
|
|
9
|
+
return context.with(ROOT_CONTEXT, () => correlationIdManager.runWithout(action));
|
|
10
|
+
}
|