@cratis/chronicle 6.42.1 → 6.43.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 +7 -6
- package/ChronicleOptions.ts +1 -1
- package/EventStore.ts +1 -1
- package/Metrics.ts +4 -0
- package/WellKnownTelemetryNames.ts +24 -5
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +7 -6
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/ChronicleOptions.d.ts +1 -1
- package/dist/ChronicleOptions.d.ts.map +1 -1
- package/dist/ChronicleOptions.js +1 -1
- package/dist/ChronicleOptions.js.map +1 -1
- package/dist/EventStore.js +1 -1
- package/dist/EventStore.js.map +1 -1
- package/dist/Metrics.d.ts +4 -0
- package/dist/Metrics.d.ts.map +1 -1
- package/dist/Metrics.js +4 -0
- package/dist/Metrics.js.map +1 -1
- package/dist/WellKnownTelemetryNames.d.ts +24 -5
- package/dist/WellKnownTelemetryNames.d.ts.map +1 -1
- package/dist/WellKnownTelemetryNames.js +24 -5
- package/dist/WellKnownTelemetryNames.js.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +31 -56
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/telemetry/ChronicleTelemetryOptions.d.ts +8 -2
- package/dist/telemetry/ChronicleTelemetryOptions.d.ts.map +1 -1
- package/dist/telemetry/observeOperation.d.ts +1 -2
- package/dist/telemetry/observeOperation.d.ts.map +1 -1
- package/dist/telemetry/observeOperation.js +2 -3
- package/dist/telemetry/observeOperation.js.map +1 -1
- package/dist/telemetry/spanAttributes.d.ts +4 -4
- package/dist/telemetry/spanAttributes.d.ts.map +1 -1
- package/dist/telemetry/spanAttributes.js +4 -7
- package/dist/telemetry/spanAttributes.js.map +1 -1
- package/eventSequences/EventSequence.ts +31 -56
- package/for_ChronicleOptions/when_configuring_telemetry/with_span_names.spec.ts +21 -21
- package/package.json +2 -2
- package/telemetry/ChronicleTelemetryOptions.ts +8 -2
- package/telemetry/for_ChronicleTelemetry/given/a_span_name_registry.fixture.ts +2 -2
- package/telemetry/for_ChronicleTelemetry/when_appending/{with_shared_and_legacy_attributes.spec.ts → with_canonical_attributes.spec.ts} +8 -9
- package/telemetry/for_ChronicleTelemetry/when_exposing_names/with_convention_names.spec.ts +65 -0
- package/telemetry/for_ChronicleTelemetry/when_naming_spans/{with_per_client_modes.spec.ts → with_convention_names.spec.ts} +45 -40
- package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts +4 -3
- package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_client_operations.spec.ts +102 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_failed_appends.spec.ts +47 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_millisecond_adapters.spec.ts +99 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_rejected_appends.spec.ts +88 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_product_attributes/with_convention_names.spec.ts +51 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_sequence_numbers/with_integer_precision.spec.ts +1 -1
- package/telemetry/observeOperation.ts +1 -4
- package/telemetry/spanAttributes.ts +5 -8
- package/telemetry/for_ChronicleTelemetry/when_exposing_names/with_legacy_compatibility.spec.ts +0 -44
- package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_legacy_overlap.spec.ts +0 -40
- package/telemetry/for_ChronicleTelemetry/when_recording_product_attributes/with_both_naming_modes.spec.ts +0 -52
|
@@ -41,7 +41,7 @@ import { observeOperation } from '../telemetry/observeOperation.js';
|
|
|
41
41
|
import { setTelemetryAttribute, setEventSourceId, setSequenceNumber, recordSafeException } from '../telemetry/spanAttributes.js';
|
|
42
42
|
import type { ChronicleTelemetryOptions } from '../telemetry/ChronicleTelemetryOptions.js';
|
|
43
43
|
import { WellKnownTelemetryNames as names } from '../WellKnownTelemetryNames.js';
|
|
44
|
-
import {
|
|
44
|
+
import { ChronicleConventionMetrics } from '../Metrics.js';
|
|
45
45
|
import { identityProvider, Identity } from '../identity/index.js';
|
|
46
46
|
import { causationManager, CausationType } from '../auditing/index.js';
|
|
47
47
|
import { toContractsGuid } from '../connection/Guid.js';
|
|
@@ -73,14 +73,14 @@ export class EventSequence implements IEventSequence {
|
|
|
73
73
|
async append(eventSourceId: string, event: object, options?: AppendOptions): Promise<AppendResult> {
|
|
74
74
|
const { eventType, correlationId, content, tags, causationChain, identity, subject } = prepareSingleAppend(event, options);
|
|
75
75
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
'chronicle.event_type_id': eventType.id.value
|
|
76
|
+
const sequenceMetricAttributes = {
|
|
77
|
+
[names.attributes.eventStore]: this._eventStoreName,
|
|
78
|
+
[names.attributes.namespace]: this._namespace,
|
|
79
|
+
[names.attributes.eventSequenceId]: this.id.value
|
|
81
80
|
};
|
|
81
|
+
const metricAttributes = { ...sequenceMetricAttributes, [names.attributes.eventTypeId]: eventType.id.value };
|
|
82
82
|
|
|
83
|
-
return observeOperation('append',
|
|
83
|
+
return observeOperation('append', async span => {
|
|
84
84
|
this.setSequenceAttributes(span);
|
|
85
85
|
setEventSourceId(span, eventSourceId, this._telemetry);
|
|
86
86
|
setTelemetryAttribute(span, 'eventTypeId', eventType.id.value);
|
|
@@ -116,7 +116,7 @@ export class EventSequence implements IEventSequence {
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
const appendResponse = ensureCommandResponse('append event', response);
|
|
119
|
-
const
|
|
119
|
+
const durationInSeconds = (performance.now() - startTime) / 1000;
|
|
120
120
|
const result = this.mapAppendResponse(
|
|
121
121
|
appendResponse.SequenceNumber,
|
|
122
122
|
appendResponse.ConstraintViolations ?? [],
|
|
@@ -126,21 +126,13 @@ export class EventSequence implements IEventSequence {
|
|
|
126
126
|
setSequenceNumber(span, result.sequenceNumber.value);
|
|
127
127
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
ChronicleConventionMetrics.eventsAppended.add(1, metricAttributes);
|
|
130
|
+
ChronicleConventionMetrics.appendDuration.record(durationInSeconds, metricAttributes);
|
|
131
131
|
if (result.constraintViolations.length > 0) {
|
|
132
|
-
|
|
133
|
-
'chronicle.event_store': this._eventStoreName,
|
|
134
|
-
'chronicle.namespace': this._namespace,
|
|
135
|
-
'chronicle.event_sequence_id': this.id.value
|
|
136
|
-
});
|
|
132
|
+
ChronicleConventionMetrics.constraintViolations.add(result.constraintViolations.length, sequenceMetricAttributes);
|
|
137
133
|
}
|
|
138
134
|
if (result.errors.length > 0) {
|
|
139
|
-
|
|
140
|
-
'chronicle.event_store': this._eventStoreName,
|
|
141
|
-
'chronicle.namespace': this._namespace,
|
|
142
|
-
'chronicle.event_sequence_id': this.id.value
|
|
143
|
-
});
|
|
135
|
+
ChronicleConventionMetrics.appendErrors.add(result.errors.length, sequenceMetricAttributes);
|
|
144
136
|
}
|
|
145
137
|
|
|
146
138
|
if (this.appendOperations.hasSubscribers) {
|
|
@@ -151,11 +143,7 @@ export class EventSequence implements IEventSequence {
|
|
|
151
143
|
return result;
|
|
152
144
|
} catch (error) {
|
|
153
145
|
recordSafeException(span, error);
|
|
154
|
-
|
|
155
|
-
'chronicle.event_store': this._eventStoreName,
|
|
156
|
-
'chronicle.namespace': this._namespace,
|
|
157
|
-
'chronicle.event_sequence_id': this.id.value
|
|
158
|
-
});
|
|
146
|
+
ChronicleConventionMetrics.appendErrors.add(1, sequenceMetricAttributes);
|
|
159
147
|
throw error;
|
|
160
148
|
} finally {
|
|
161
149
|
span.end();
|
|
@@ -177,13 +165,12 @@ export class EventSequence implements IEventSequence {
|
|
|
177
165
|
const distinctEventSourceIds = [...new Set(eventsForEventSourceIds.map(_ => _.eventSourceId))];
|
|
178
166
|
|
|
179
167
|
const batchMetricAttributes = {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
'chronicle.events_count': eventsForEventSourceIds.length
|
|
168
|
+
[names.attributes.eventStore]: this._eventStoreName,
|
|
169
|
+
[names.attributes.namespace]: this._namespace,
|
|
170
|
+
[names.attributes.eventSequenceId]: this.id.value
|
|
184
171
|
};
|
|
185
172
|
|
|
186
|
-
return observeOperation('appendMany',
|
|
173
|
+
return observeOperation('appendMany', async span => {
|
|
187
174
|
this.setSequenceAttributes(span);
|
|
188
175
|
if (distinctEventSourceIds.length === 1) {
|
|
189
176
|
setEventSourceId(span, distinctEventSourceIds[0], this._telemetry);
|
|
@@ -210,7 +197,7 @@ export class EventSequence implements IEventSequence {
|
|
|
210
197
|
});
|
|
211
198
|
|
|
212
199
|
const appendManyResponse = ensureCommandResponse('append many events', response);
|
|
213
|
-
const
|
|
200
|
+
const durationInSeconds = (performance.now() - startTime) / 1000;
|
|
214
201
|
// Mirrors the C# client: every per-event AppendResult in a batch carries all
|
|
215
202
|
// constraint violations and the first concurrency violation of the whole batch —
|
|
216
203
|
// the wire response doesn't correlate either back to a specific event index.
|
|
@@ -236,25 +223,17 @@ export class EventSequence implements IEventSequence {
|
|
|
236
223
|
);
|
|
237
224
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
238
225
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
226
|
+
ChronicleConventionMetrics.batchAppendsPerformed.add(1, batchMetricAttributes);
|
|
227
|
+
ChronicleConventionMetrics.eventsAppended.add(eventsForEventSourceIds.length, batchMetricAttributes);
|
|
228
|
+
ChronicleConventionMetrics.appendManyDuration.record(durationInSeconds, batchMetricAttributes);
|
|
242
229
|
|
|
243
230
|
const totalViolations = result.reduce((sum: number, appendResult: AppendResult) => sum + appendResult.constraintViolations.length, 0);
|
|
244
231
|
if (totalViolations > 0) {
|
|
245
|
-
|
|
246
|
-
'chronicle.event_store': this._eventStoreName,
|
|
247
|
-
'chronicle.namespace': this._namespace,
|
|
248
|
-
'chronicle.event_sequence_id': this.id.value
|
|
249
|
-
});
|
|
232
|
+
ChronicleConventionMetrics.constraintViolations.add(totalViolations, batchMetricAttributes);
|
|
250
233
|
}
|
|
251
234
|
const totalErrors = result.reduce((sum: number, appendResult: AppendResult) => sum + appendResult.errors.length, 0);
|
|
252
235
|
if (totalErrors > 0) {
|
|
253
|
-
|
|
254
|
-
'chronicle.event_store': this._eventStoreName,
|
|
255
|
-
'chronicle.namespace': this._namespace,
|
|
256
|
-
'chronicle.event_sequence_id': this.id.value
|
|
257
|
-
});
|
|
236
|
+
ChronicleConventionMetrics.appendErrors.add(totalErrors, batchMetricAttributes);
|
|
258
237
|
}
|
|
259
238
|
|
|
260
239
|
if (this.appendOperations.hasSubscribers && result.length > 0) {
|
|
@@ -270,11 +249,7 @@ export class EventSequence implements IEventSequence {
|
|
|
270
249
|
return result;
|
|
271
250
|
} catch (error) {
|
|
272
251
|
recordSafeException(span, error);
|
|
273
|
-
|
|
274
|
-
'chronicle.event_store': this._eventStoreName,
|
|
275
|
-
'chronicle.namespace': this._namespace,
|
|
276
|
-
'chronicle.event_sequence_id': this.id.value
|
|
277
|
-
});
|
|
252
|
+
ChronicleConventionMetrics.appendErrors.add(1, batchMetricAttributes);
|
|
278
253
|
throw error;
|
|
279
254
|
} finally {
|
|
280
255
|
span.end();
|
|
@@ -299,7 +274,7 @@ export class EventSequence implements IEventSequence {
|
|
|
299
274
|
eventStreamId?: string,
|
|
300
275
|
filterEventTypes?: Constructor[]
|
|
301
276
|
): Promise<EventSequenceNumber> {
|
|
302
|
-
return observeOperation('getTailSequenceNumber',
|
|
277
|
+
return observeOperation('getTailSequenceNumber', async span => {
|
|
303
278
|
this.setSequenceAttributes(span);
|
|
304
279
|
setEventSourceId(span, eventSourceId, this._telemetry);
|
|
305
280
|
try {
|
|
@@ -339,7 +314,7 @@ export class EventSequence implements IEventSequence {
|
|
|
339
314
|
|
|
340
315
|
/** @inheritdoc */
|
|
341
316
|
async hasEventsFor(eventSourceId: string): Promise<boolean> {
|
|
342
|
-
return observeOperation('hasEventsFor',
|
|
317
|
+
return observeOperation('hasEventsFor', async span => {
|
|
343
318
|
this.setSequenceAttributes(span);
|
|
344
319
|
setEventSourceId(span, eventSourceId, this._telemetry);
|
|
345
320
|
try {
|
|
@@ -371,7 +346,7 @@ export class EventSequence implements IEventSequence {
|
|
|
371
346
|
eventStreamId?: string,
|
|
372
347
|
eventSourceType?: string
|
|
373
348
|
): Promise<AppendedEvent[]> {
|
|
374
|
-
return observeOperation('getForEventSourceIdAndEventTypes',
|
|
349
|
+
return observeOperation('getForEventSourceIdAndEventTypes', async span => {
|
|
375
350
|
this.setSequenceAttributes(span);
|
|
376
351
|
setEventSourceId(span, eventSourceId, this._telemetry);
|
|
377
352
|
try {
|
|
@@ -406,7 +381,7 @@ export class EventSequence implements IEventSequence {
|
|
|
406
381
|
eventSourceId?: string,
|
|
407
382
|
filterEventTypes?: Constructor[]
|
|
408
383
|
): Promise<AppendedEvent[]> {
|
|
409
|
-
return observeOperation('getFromSequenceNumber',
|
|
384
|
+
return observeOperation('getFromSequenceNumber', async span => {
|
|
410
385
|
this.setSequenceAttributes(span);
|
|
411
386
|
setSequenceNumber(span, sequenceNumber.value);
|
|
412
387
|
setEventSourceId(span, eventSourceId, this._telemetry);
|
|
@@ -438,7 +413,7 @@ export class EventSequence implements IEventSequence {
|
|
|
438
413
|
const causationChain = causationManager.getCurrentChain();
|
|
439
414
|
const identity = identityProvider.getCurrent();
|
|
440
415
|
|
|
441
|
-
return observeOperation('redact',
|
|
416
|
+
return observeOperation('redact', async span => {
|
|
442
417
|
this.setSequenceAttributes(span);
|
|
443
418
|
setSequenceNumber(span, sequenceNumber.value);
|
|
444
419
|
try {
|
|
@@ -472,7 +447,7 @@ export class EventSequence implements IEventSequence {
|
|
|
472
447
|
const identity = identityProvider.getCurrent();
|
|
473
448
|
const wireEventTypeIds = (eventTypes ?? []).map(constructor => getEventTypeFor(constructor as unknown as Function).id.value);
|
|
474
449
|
|
|
475
|
-
return observeOperation('redactForEventSource',
|
|
450
|
+
return observeOperation('redactForEventSource', async span => {
|
|
476
451
|
this.setSequenceAttributes(span);
|
|
477
452
|
setEventSourceId(span, eventSourceId, this._telemetry);
|
|
478
453
|
try {
|
|
@@ -518,7 +493,7 @@ export class EventSequence implements IEventSequence {
|
|
|
518
493
|
|
|
519
494
|
/** @inheritdoc */
|
|
520
495
|
async completeStream(eventStreamType: string, eventStreamId: string): Promise<CompleteStreamResult> {
|
|
521
|
-
return observeOperation('completeStream',
|
|
496
|
+
return observeOperation('completeStream', async span => {
|
|
522
497
|
this.setSequenceAttributes(span);
|
|
523
498
|
setTelemetryAttribute(span, 'eventStreamType', eventStreamType);
|
|
524
499
|
setTelemetryAttribute(span, 'eventStreamId', eventStreamId);
|
|
@@ -6,27 +6,27 @@ import { ChronicleOptions } from '../../ChronicleOptions.js';
|
|
|
6
6
|
import type { ChronicleTelemetryOptions } from '../../telemetry/ChronicleTelemetryOptions.js';
|
|
7
7
|
|
|
8
8
|
chai.should();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
it('should preserve the choice with the event source identifier policy', () => {
|
|
25
|
-
const telemetry = { spanNames, eventSourceId: { mode: 'raw' as const } };
|
|
26
|
-
const development = ChronicleOptions.development({ telemetry });
|
|
27
|
-
const connection = ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { telemetry });
|
|
28
|
-
development.telemetry!.should.deep.equal(telemetry);
|
|
29
|
-
connection.telemetry!.should.deep.equal(telemetry);
|
|
9
|
+
const factories = {
|
|
10
|
+
development: (telemetry: ChronicleTelemetryOptions) => ChronicleOptions.development({ telemetry }),
|
|
11
|
+
connectionString: (telemetry: ChronicleTelemetryOptions) => ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { telemetry })
|
|
12
|
+
};
|
|
13
|
+
for (const [name, factory] of Object.entries(factories)) {
|
|
14
|
+
describe(`when configuring compatibility span names through ${name}`, () => {
|
|
15
|
+
for (const spanNames of ['legacy', 'convention'] as const) {
|
|
16
|
+
it(`should accept ${spanNames} without throwing or changing privacy`, () => {
|
|
17
|
+
factory({ spanNames, eventSourceId: { mode: 'raw' } }).telemetry!.should.deep.equal({
|
|
18
|
+
spanNames, eventSourceId: { mode: 'raw' }
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
it('should accept privacy settings without a span name option', () => {
|
|
23
|
+
factory({ eventSourceId: { mode: 'raw' } }).telemetry!.eventSourceId!.should.deep.equal({ mode: 'raw' });
|
|
30
24
|
});
|
|
25
|
+
for (const spanNames of ['invalid', '', null, 42, {}]) {
|
|
26
|
+
it(`should reject the invalid JavaScript selector ${JSON.stringify(spanNames)}`, () => {
|
|
27
|
+
const telemetry = { spanNames } as unknown as ChronicleTelemetryOptions;
|
|
28
|
+
(() => factory(telemetry)).should.throw(TypeError, 'telemetry.spanNames must be legacy or convention.');
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
31
|
});
|
|
32
32
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/chronicle",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.43.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",
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
"clean": "rm -rf dist",
|
|
174
174
|
"test": "vitest run",
|
|
175
175
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
176
|
-
"test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/declarative/addChildCompatibility.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts events/for_handles/when_decorating_a_method.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_joins/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_field_types/with_standard_decorators.standard.spec.ts eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_tags/with_standard_decorators.standard.spec.ts",
|
|
176
|
+
"test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/declarative/addChildCompatibility.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts events/for_handles/when_decorating_a_method.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_joins/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_field_types/with_standard_decorators.standard.spec.ts eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_tags/with_standard_decorators.standard.spec.ts telemetry/for_ChronicleTelemetry/when_exposing_names/with_convention_names.spec.ts",
|
|
177
177
|
"test:native": "node ./scripts/check-native-esm.mjs"
|
|
178
178
|
},
|
|
179
179
|
"dependencies": {
|
|
@@ -1,9 +1,15 @@
|
|
|
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
|
-
/** Per-client
|
|
4
|
+
/** Per-client privacy and compatibility settings; instrumentation itself is always available. */
|
|
5
5
|
export interface ChronicleTelemetryOptions {
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Retained naming selector. Both 'legacy' and 'convention' are accepted as no-ops;
|
|
8
|
+
* built-in instrumentation always uses conventionSpans, including when this option is absent.
|
|
9
|
+
* @deprecated No longer needed; built-in spans always use convention names. This option has no removal deadline.
|
|
10
|
+
* The minor-release naming cutoff replaces the earlier "next major" plan after ADR 0001's one-minor overlap.
|
|
11
|
+
* See https://github.com/Cratis/Chronicle.TypeScript/issues/171.
|
|
12
|
+
*/
|
|
7
13
|
spanNames?: 'legacy' | 'convention';
|
|
8
14
|
|
|
9
15
|
/** Absent means no event source identifiers. HMAC uses SHA-256 and a deployment-owned key. */
|
|
@@ -15,7 +15,7 @@ export const legacySpans = {
|
|
|
15
15
|
getEventStore: 'chronicle.client.get_event_store',
|
|
16
16
|
getEventStores: 'chronicle.client.get_event_stores',
|
|
17
17
|
getNamespaces: 'chronicle.event_store.get_namespaces'
|
|
18
|
-
};
|
|
18
|
+
} as const;
|
|
19
19
|
|
|
20
20
|
export const conventionSpans = {
|
|
21
21
|
append: 'cratis.chronicle.client.event_sequence.append',
|
|
@@ -30,4 +30,4 @@ export const conventionSpans = {
|
|
|
30
30
|
getEventStore: 'cratis.chronicle.client.event_store.get',
|
|
31
31
|
getEventStores: 'cratis.chronicle.client.event_store.list',
|
|
32
32
|
getNamespaces: 'cratis.chronicle.client.event_store.get_namespaces'
|
|
33
|
-
};
|
|
33
|
+
} as const;
|
|
@@ -26,24 +26,23 @@ describe('when appending with a business correlation override', () => {
|
|
|
26
26
|
await correlationIdManager.run(new CorrelationId('outer'), () => sequence.append('sensitive-id', new Recorded(), { correlationId: correlation }));
|
|
27
27
|
span = telemetry.spans.getFinishedSpans()[0];
|
|
28
28
|
});
|
|
29
|
-
it('should produce one client span with the
|
|
29
|
+
it('should produce one client span with the convention operation name', () => {
|
|
30
30
|
telemetry.spans.getFinishedSpans().should.have.lengthOf(1);
|
|
31
31
|
span.kind.should.equal(SpanKind.CLIENT);
|
|
32
|
-
span.name.should.equal('chronicle.
|
|
32
|
+
span.name.should.equal('cratis.chronicle.client.event_sequence.append');
|
|
33
33
|
});
|
|
34
34
|
it('should use the versioned shared instrumentation scope', () => {
|
|
35
35
|
span.instrumentationScope.name.should.equal('Cratis.Chronicle.Client');
|
|
36
36
|
should.equal(span.instrumentationScope.version, clientVersion);
|
|
37
37
|
});
|
|
38
|
-
it('should emit
|
|
38
|
+
it('should emit canonical attributes with the exact-string sequence number exception', () => {
|
|
39
39
|
span.attributes.should.include({
|
|
40
|
-
'cratis.event_store.name': 'store', '
|
|
41
|
-
'cratis.
|
|
42
|
-
'cratis.
|
|
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'
|
|
40
|
+
'cratis.event_store.name': 'store', 'cratis.event_store.namespace': 'namespace',
|
|
41
|
+
'cratis.event_sequence.id': 'event-log', 'cratis.event_type.id': 'telemetry-recorded',
|
|
42
|
+
'cratis.event_type.generation': 1, 'cratis.event_sequence.number': 42
|
|
46
43
|
});
|
|
44
|
+
Object.keys(span.attributes).filter(key => key.startsWith('chronicle.')).should.deep.equal(['chronicle.sequence_number']);
|
|
45
|
+
span.attributes.should.have.property('chronicle.sequence_number', '42');
|
|
47
46
|
});
|
|
48
47
|
it('should scope the resolved correlation consistently for propagation', () => {
|
|
49
48
|
should.equal(span.attributes['cratis.correlation_id'], correlation);
|
|
@@ -0,0 +1,65 @@
|
|
|
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, expectTypeOf, it } from 'vitest';
|
|
5
|
+
import { ChronicleOptions, ChronicleMetrics, ChronicleConventionMetrics, WellKnownTelemetryNames as names } from '../../../index.js';
|
|
6
|
+
import type { ChronicleTelemetryOptions } from '../../../index.js';
|
|
7
|
+
import { legacySpans, conventionSpans } from '../given/a_span_name_registry.fixture.js';
|
|
8
|
+
|
|
9
|
+
const legacyAttributes = {
|
|
10
|
+
eventStore: 'chronicle.event_store', namespace: 'chronicle.namespace',
|
|
11
|
+
eventSequenceId: 'chronicle.event_sequence_id', sequenceNumber: 'chronicle.sequence_number',
|
|
12
|
+
eventTypeId: 'chronicle.event_type_id', eventTypeGeneration: 'chronicle.event_type_generation',
|
|
13
|
+
eventSourceId: 'chronicle.event_source_id', eventCount: 'chronicle.events_count',
|
|
14
|
+
hasEvents: 'chronicle.has_events', eventStreamType: 'chronicle.event_stream_type', eventStreamId: 'chronicle.event_stream_id'
|
|
15
|
+
} as const;
|
|
16
|
+
const legacyMetrics = {
|
|
17
|
+
eventsAppended: 'chronicle.events.appended', batchAppendsPerformed: 'chronicle.events.batch_appends',
|
|
18
|
+
eventStoreRetrievals: 'chronicle.client.event_store_retrievals', appendDuration: 'chronicle.events.append_duration',
|
|
19
|
+
appendManyDuration: 'chronicle.events.append_many_duration', constraintViolations: 'chronicle.events.constraint_violations',
|
|
20
|
+
appendErrors: 'chronicle.events.append_errors'
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
chai.should();
|
|
24
|
+
describe('when exposing canonical names alongside retained compatibility APIs', () => {
|
|
25
|
+
it('should preserve every legacy span value and literal type', () => {
|
|
26
|
+
names.spans.should.deep.equal(legacySpans);
|
|
27
|
+
expectTypeOf(names.spans).toEqualTypeOf<typeof legacySpans>();
|
|
28
|
+
});
|
|
29
|
+
it('should expose a matching canonical span for every operation', () => {
|
|
30
|
+
names.conventionSpans.should.deep.equal(conventionSpans);
|
|
31
|
+
expectTypeOf(names.conventionSpans).toEqualTypeOf<typeof conventionSpans>();
|
|
32
|
+
Object.keys(names.conventionSpans).should.deep.equal(Object.keys(names.spans));
|
|
33
|
+
});
|
|
34
|
+
it('should preserve the current and historical scopes', () => {
|
|
35
|
+
names.scope.should.equal('Cratis.Chronicle.Client');
|
|
36
|
+
names.legacyScope.should.equal('@cratis/chronicle');
|
|
37
|
+
expectTypeOf(names.legacyScope).toEqualTypeOf<'@cratis/chronicle'>();
|
|
38
|
+
});
|
|
39
|
+
it('should preserve every legacy attribute value and literal type', () => {
|
|
40
|
+
names.legacyAttributes.should.deep.equal(legacyAttributes);
|
|
41
|
+
expectTypeOf(names.legacyAttributes).toEqualTypeOf<typeof legacyAttributes>();
|
|
42
|
+
});
|
|
43
|
+
it('should preserve every legacy metric value and literal type', () => {
|
|
44
|
+
names.legacyMetrics.should.deep.equal(legacyMetrics);
|
|
45
|
+
expectTypeOf(names.legacyMetrics).toEqualTypeOf<typeof legacyMetrics>();
|
|
46
|
+
});
|
|
47
|
+
it('should retain both typed selector values through the public factories', () => {
|
|
48
|
+
expectTypeOf<ChronicleTelemetryOptions['spanNames']>().toEqualTypeOf<'legacy' | 'convention' | undefined>();
|
|
49
|
+
for (const spanNames of ['legacy', 'convention'] as const) {
|
|
50
|
+
ChronicleOptions.development({ telemetry: { spanNames } }).telemetry!.spanNames!.should.equal(spanNames);
|
|
51
|
+
ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { telemetry: { spanNames } })
|
|
52
|
+
.telemetry!.spanNames!.should.equal(spanNames);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
it('should retain the public metric recording signatures', () => {
|
|
56
|
+
expectTypeOf(ChronicleMetrics).toEqualTypeOf<typeof ChronicleConventionMetrics>();
|
|
57
|
+
Object.keys(ChronicleMetrics).should.have.members(Object.keys(names.legacyMetrics));
|
|
58
|
+
});
|
|
59
|
+
it('should retain the product-only attributes alongside the shared registry', () => {
|
|
60
|
+
names.attributes.should.include({
|
|
61
|
+
hasEvents: 'cratis.chronicle.event_sequence.has_events',
|
|
62
|
+
eventStreamType: 'cratis.chronicle.event_stream.type', eventStreamId: 'cratis.chronicle.event_stream.id'
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -6,6 +6,7 @@ import { SpanKind } from '@opentelemetry/api';
|
|
|
6
6
|
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
7
7
|
import type { ChronicleClient } from '../../../ChronicleClient.js';
|
|
8
8
|
import type { IEventSequence } from '../../../eventSequences/IEventSequence.js';
|
|
9
|
+
import type { ChronicleTelemetryOptions } from '../../ChronicleTelemetryOptions.js';
|
|
9
10
|
import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
|
|
10
11
|
import { EventSequenceNumber } from '../../../eventSequences/EventSequenceNumber.js';
|
|
11
12
|
import { clientVersion } from '../../../connection/clientVersion.js';
|
|
@@ -30,14 +31,14 @@ const operations = {
|
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
for (const spanNames of [undefined, 'legacy', 'convention'] as const) {
|
|
33
|
-
const
|
|
34
|
+
const options: ChronicleTelemetryOptions | undefined = spanNames === undefined ? undefined : { spanNames };
|
|
34
35
|
for (const sequenceId of [EventSequenceId.eventLog, new EventSequenceId('dynamic')]) {
|
|
35
36
|
for (const operation of Object.keys(operations) as (keyof typeof operations)[]) {
|
|
36
|
-
describe(`when naming ${operation} on ${sequenceId.value} with ${spanNames ?? 'default'}
|
|
37
|
+
describe(`when naming ${operation} on ${sequenceId.value} with ${spanNames ?? 'default'} configuration`, () => {
|
|
37
38
|
let instance: ChronicleClient;
|
|
38
39
|
let span: ReadableSpan;
|
|
39
40
|
beforeEach(async () => {
|
|
40
|
-
instance = client(
|
|
41
|
+
instance = client(options);
|
|
41
42
|
const store = await instance.getEventStore('store', 'namespace');
|
|
42
43
|
const sequence = sequenceId === EventSequenceId.eventLog ? store.eventLog : store.getEventSequence(sequenceId);
|
|
43
44
|
telemetry.spans.reset();
|
|
@@ -45,32 +46,35 @@ for (const spanNames of [undefined, 'legacy', 'convention'] as const) {
|
|
|
45
46
|
span = telemetry.spans.getFinishedSpans()[0];
|
|
46
47
|
});
|
|
47
48
|
afterEach(() => instance.dispose());
|
|
48
|
-
it('should emit exactly one client span
|
|
49
|
+
it('should emit exactly one convention client span and no legacy span', () => {
|
|
49
50
|
telemetry.spans.getFinishedSpans().should.have.lengthOf(1);
|
|
50
|
-
span.name.should.equal(
|
|
51
|
+
span.name.should.equal(conventionSpans[operation]).and.not.equal(legacySpans[operation]);
|
|
51
52
|
span.kind.should.equal(SpanKind.CLIENT);
|
|
52
53
|
});
|
|
53
54
|
it('should retain the versioned shared instrumentation scope', () => {
|
|
54
55
|
span.instrumentationScope.name.should.equal('Cratis.Chronicle.Client');
|
|
55
56
|
should.equal(span.instrumentationScope.version, clientVersion);
|
|
56
57
|
});
|
|
57
|
-
it('should
|
|
58
|
+
it('should emit canonical attributes with only the exact-string sequence number exception', () => {
|
|
58
59
|
span.attributes.should.include({
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'chronicle.event_sequence_id': sequenceId.value, 'cratis.event_sequence.id': sequenceId.value
|
|
60
|
+
'cratis.event_store.name': 'store', 'cratis.event_store.namespace': 'namespace',
|
|
61
|
+
'cratis.event_sequence.id': sequenceId.value
|
|
62
62
|
});
|
|
63
|
+
const hasSequenceNumber = ['append', 'getTailSequenceNumber', 'getFromSequenceNumber', 'redact'].includes(operation);
|
|
64
|
+
Object.keys(span.attributes).filter(key => key.startsWith('chronicle.')).should.deep.equal(
|
|
65
|
+
hasSequenceNumber ? ['chronicle.sequence_number'] : []);
|
|
66
|
+
if (hasSequenceNumber) span.attributes.should.have.property('chronicle.sequence_number', '42');
|
|
63
67
|
});
|
|
64
68
|
});
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
for (const operation of ['getEventStore', 'getEventStores', 'getNamespaces'] as const) {
|
|
69
|
-
describe(`when naming ${operation} with ${spanNames ?? 'default'}
|
|
73
|
+
describe(`when naming ${operation} with ${spanNames ?? 'default'} configuration`, () => {
|
|
70
74
|
let instance: ChronicleClient;
|
|
71
75
|
let span: ReadableSpan;
|
|
72
76
|
beforeEach(async () => {
|
|
73
|
-
instance = client(
|
|
77
|
+
instance = client(options);
|
|
74
78
|
if (operation === 'getNamespaces') {
|
|
75
79
|
const store = await instance.getEventStore('store', 'namespace');
|
|
76
80
|
telemetry.spans.reset();
|
|
@@ -83,56 +87,57 @@ for (const spanNames of [undefined, 'legacy', 'convention'] as const) {
|
|
|
83
87
|
span = telemetry.spans.getFinishedSpans()[0];
|
|
84
88
|
});
|
|
85
89
|
afterEach(() => instance.dispose());
|
|
86
|
-
it('should emit exactly one client span
|
|
90
|
+
it('should emit exactly one convention client span and no legacy span', () => {
|
|
87
91
|
telemetry.spans.getFinishedSpans().should.have.lengthOf(1);
|
|
88
|
-
span.name.should.equal(
|
|
92
|
+
span.name.should.equal(conventionSpans[operation]).and.not.equal(legacySpans[operation]);
|
|
89
93
|
span.kind.should.equal(SpanKind.CLIENT);
|
|
90
94
|
span.instrumentationScope.name.should.equal('Cratis.Chronicle.Client');
|
|
91
95
|
should.equal(span.instrumentationScope.version, clientVersion);
|
|
92
96
|
});
|
|
97
|
+
it('should not emit legacy attributes', () => {
|
|
98
|
+
Object.keys(span.attributes).filter(key => key.startsWith('chronicle.')).should.be.empty;
|
|
99
|
+
});
|
|
93
100
|
if (operation !== 'getEventStores') {
|
|
94
|
-
it('should retain
|
|
95
|
-
span.attributes.should.include({ '
|
|
101
|
+
it('should retain the canonical event store attribute', () => {
|
|
102
|
+
span.attributes.should.include({ 'cratis.event_store.name': 'store' });
|
|
96
103
|
});
|
|
97
104
|
}
|
|
98
105
|
});
|
|
99
106
|
}
|
|
100
107
|
}
|
|
101
108
|
|
|
102
|
-
describe('when
|
|
109
|
+
describe('when legacy and convention configurations coexist in one process', () => {
|
|
103
110
|
let legacy: ChronicleClient;
|
|
104
111
|
let convention: ChronicleClient;
|
|
105
|
-
let spans: ReadableSpan[];
|
|
106
112
|
beforeEach(async () => {
|
|
107
|
-
legacy = client({ spanNames: 'legacy' });
|
|
113
|
+
legacy = client({ spanNames: 'legacy', eventSourceId: { mode: 'raw' } });
|
|
108
114
|
convention = client({ spanNames: 'convention' });
|
|
109
115
|
const [legacyStore, conventionStore] = await Promise.all([
|
|
110
|
-
legacy.getEventStore('legacy-
|
|
116
|
+
legacy.getEventStore('legacy-client'), convention.getEventStore('convention-client')
|
|
117
|
+
]);
|
|
118
|
+
telemetry.spans.reset();
|
|
119
|
+
await Promise.all([
|
|
120
|
+
legacyStore.eventLog.append('legacy-source', new Recorded()),
|
|
121
|
+
conventionStore.eventLog.append('convention-source', new Recorded())
|
|
111
122
|
]);
|
|
112
|
-
await Promise.all([convention.getEventStores(), legacy.getEventStores()]);
|
|
113
|
-
await Promise.all([legacyStore.getNamespaces(), conventionStore.getNamespaces()]);
|
|
114
|
-
await conventionStore.eventLog.append('source', new Recorded());
|
|
115
|
-
await legacyStore.eventLog.append('source', new Recorded());
|
|
116
|
-
const id = new EventSequenceId('dynamic');
|
|
117
|
-
await legacyStore.getEventSequence(id).append('source', new Recorded());
|
|
118
|
-
await conventionStore.getEventSequence(id).append('source', new Recorded());
|
|
119
|
-
await legacy.getEventStore('legacy-store');
|
|
120
|
-
await convention.getEventStore('convention-store');
|
|
121
|
-
spans = telemetry.spans.getFinishedSpans();
|
|
122
123
|
});
|
|
123
124
|
afterEach(() => { legacy.dispose(); convention.dispose(); });
|
|
124
|
-
it('should
|
|
125
|
-
spans.should.
|
|
126
|
-
|
|
127
|
-
legacySpans.getEventStore, legacySpans.getNamespaces, legacySpans.append, legacySpans.append, legacySpans.getEventStore
|
|
125
|
+
it('should emit one canonical span per operation regardless of the retained selector', () => {
|
|
126
|
+
telemetry.spans.getFinishedSpans().map(span => span.name).should.deep.equal([
|
|
127
|
+
conventionSpans.append, conventionSpans.append
|
|
128
128
|
]);
|
|
129
|
-
spans.filter(span => span.attributes['cratis.event_store.name'] === 'convention-store').map(span => span.name).should.deep.equal([
|
|
130
|
-
conventionSpans.getEventStore, conventionSpans.getNamespaces, conventionSpans.append, conventionSpans.append, conventionSpans.getEventStore
|
|
131
|
-
]);
|
|
132
|
-
spans.map(span => span.name).should.include(legacySpans.getEventStores).and.include(conventionSpans.getEventStores);
|
|
133
129
|
});
|
|
134
|
-
it('should
|
|
135
|
-
|
|
136
|
-
|
|
130
|
+
it('should keep privacy local to each client', () => {
|
|
131
|
+
const spans = telemetry.spans.getFinishedSpans();
|
|
132
|
+
spans.find(span => span.attributes['cratis.event_store.name'] === 'legacy-client')!
|
|
133
|
+
.attributes.should.have.property('cratis.event_source.id', 'legacy-source');
|
|
134
|
+
spans.find(span => span.attributes['cratis.event_store.name'] === 'convention-client')!
|
|
135
|
+
.attributes.should.not.have.property('cratis.event_source.id');
|
|
136
|
+
});
|
|
137
|
+
it('should not restore legacy metric instruments for either client', async () => {
|
|
138
|
+
const scopes = (await telemetry.reader.collect()).resourceMetrics.scopeMetrics;
|
|
139
|
+
scopes.should.have.lengthOf(1);
|
|
140
|
+
scopes[0].metrics.should.not.be.empty;
|
|
141
|
+
scopes[0].metrics.filter(metric => metric.descriptor.name.startsWith('chronicle.')).should.be.empty;
|
|
137
142
|
});
|
|
138
143
|
});
|
package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts
CHANGED
|
@@ -32,11 +32,11 @@ for (const [label, policy, expected] of [
|
|
|
32
32
|
await sequence.redactForEventSource(source, 'private reason');
|
|
33
33
|
services.append.mock.calls[0][0].EventSourceId.should.equal(source);
|
|
34
34
|
});
|
|
35
|
-
it('should apply the policy
|
|
35
|
+
it('should apply the policy only to the canonical name on every operation', () => {
|
|
36
36
|
telemetry.spans.getFinishedSpans().should.have.lengthOf(8);
|
|
37
37
|
for (const span of telemetry.spans.getFinishedSpans()) {
|
|
38
38
|
should.equal(span.attributes['cratis.event_source.id'], expected);
|
|
39
|
-
should.equal(span.attributes['chronicle.event_source_id'],
|
|
39
|
+
should.equal(span.attributes['chronicle.event_source_id'], undefined);
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
});
|
|
@@ -71,7 +71,8 @@ describe('when appending a batch with multiple event sources', () => {
|
|
|
71
71
|
});
|
|
72
72
|
it('should record the count without an identifier array', () => {
|
|
73
73
|
const attributes = telemetry.spans.getFinishedSpans()[0].attributes;
|
|
74
|
-
attributes.should.
|
|
74
|
+
attributes.should.have.property('cratis.event.count', 2);
|
|
75
|
+
attributes.should.not.have.property('chronicle.events_count');
|
|
75
76
|
should.equal(attributes['cratis.event_source.id'], undefined);
|
|
76
77
|
should.equal(attributes['chronicle.event_source_id'], undefined);
|
|
77
78
|
});
|