@cratis/chronicle 6.42.2 → 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.
Files changed (53) hide show
  1. package/ChronicleClient.ts +7 -6
  2. package/ChronicleOptions.ts +1 -1
  3. package/EventStore.ts +1 -1
  4. package/Metrics.ts +3 -3
  5. package/WellKnownTelemetryNames.ts +9 -8
  6. package/dist/ChronicleClient.d.ts.map +1 -1
  7. package/dist/ChronicleClient.js +7 -6
  8. package/dist/ChronicleClient.js.map +1 -1
  9. package/dist/ChronicleOptions.d.ts +1 -1
  10. package/dist/ChronicleOptions.d.ts.map +1 -1
  11. package/dist/ChronicleOptions.js +1 -1
  12. package/dist/ChronicleOptions.js.map +1 -1
  13. package/dist/EventStore.js +1 -1
  14. package/dist/EventStore.js.map +1 -1
  15. package/dist/Metrics.d.ts +3 -3
  16. package/dist/Metrics.js +3 -3
  17. package/dist/WellKnownTelemetryNames.d.ts +9 -8
  18. package/dist/WellKnownTelemetryNames.d.ts.map +1 -1
  19. package/dist/WellKnownTelemetryNames.js +9 -8
  20. package/dist/WellKnownTelemetryNames.js.map +1 -1
  21. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  22. package/dist/eventSequences/EventSequence.js +31 -56
  23. package/dist/eventSequences/EventSequence.js.map +1 -1
  24. package/dist/telemetry/ChronicleTelemetryOptions.d.ts +5 -4
  25. package/dist/telemetry/ChronicleTelemetryOptions.d.ts.map +1 -1
  26. package/dist/telemetry/observeOperation.d.ts +1 -2
  27. package/dist/telemetry/observeOperation.d.ts.map +1 -1
  28. package/dist/telemetry/observeOperation.js +2 -3
  29. package/dist/telemetry/observeOperation.js.map +1 -1
  30. package/dist/telemetry/spanAttributes.d.ts +4 -4
  31. package/dist/telemetry/spanAttributes.d.ts.map +1 -1
  32. package/dist/telemetry/spanAttributes.js +4 -7
  33. package/dist/telemetry/spanAttributes.js.map +1 -1
  34. package/eventSequences/EventSequence.ts +31 -56
  35. package/for_ChronicleOptions/when_configuring_telemetry/with_span_names.spec.ts +21 -21
  36. package/package.json +2 -2
  37. package/telemetry/ChronicleTelemetryOptions.ts +5 -4
  38. package/telemetry/for_ChronicleTelemetry/given/a_span_name_registry.fixture.ts +2 -2
  39. package/telemetry/for_ChronicleTelemetry/when_appending/{with_shared_and_legacy_attributes.spec.ts → with_canonical_attributes.spec.ts} +8 -9
  40. package/telemetry/for_ChronicleTelemetry/when_exposing_names/with_convention_names.spec.ts +65 -0
  41. package/telemetry/for_ChronicleTelemetry/when_naming_spans/{with_per_client_modes.spec.ts → with_convention_names.spec.ts} +45 -40
  42. package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts +4 -3
  43. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_client_operations.spec.ts +102 -0
  44. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_failed_appends.spec.ts +47 -0
  45. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_millisecond_adapters.spec.ts +99 -0
  46. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_rejected_appends.spec.ts +88 -0
  47. package/telemetry/for_ChronicleTelemetry/when_recording_product_attributes/with_convention_names.spec.ts +51 -0
  48. package/telemetry/for_ChronicleTelemetry/when_recording_sequence_numbers/with_integer_precision.spec.ts +1 -1
  49. package/telemetry/observeOperation.ts +1 -4
  50. package/telemetry/spanAttributes.ts +5 -8
  51. package/telemetry/for_ChronicleTelemetry/when_exposing_names/with_legacy_compatibility.spec.ts +0 -44
  52. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_legacy_overlap.spec.ts +0 -40
  53. 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 { ChronicleMetrics } from '../Metrics.js';
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 metricAttributes = {
77
- 'chronicle.event_store': this._eventStoreName,
78
- 'chronicle.namespace': this._namespace,
79
- 'chronicle.event_sequence_id': this.id.value,
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', this._telemetry, async span => {
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 duration = performance.now() - startTime;
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
- ChronicleMetrics.eventsAppended.add(1, metricAttributes);
130
- ChronicleMetrics.appendDuration.record(duration, metricAttributes);
129
+ ChronicleConventionMetrics.eventsAppended.add(1, metricAttributes);
130
+ ChronicleConventionMetrics.appendDuration.record(durationInSeconds, metricAttributes);
131
131
  if (result.constraintViolations.length > 0) {
132
- ChronicleMetrics.constraintViolations.add(result.constraintViolations.length, {
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
- ChronicleMetrics.appendErrors.add(result.errors.length, {
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
- ChronicleMetrics.appendErrors.add(1, {
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
- 'chronicle.event_store': this._eventStoreName,
181
- 'chronicle.namespace': this._namespace,
182
- 'chronicle.event_sequence_id': this.id.value,
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', this._telemetry, async span => {
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 duration = performance.now() - startTime;
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
- ChronicleMetrics.batchAppendsPerformed.add(1, batchMetricAttributes);
240
- ChronicleMetrics.eventsAppended.add(eventsForEventSourceIds.length, batchMetricAttributes);
241
- ChronicleMetrics.appendManyDuration.record(duration, batchMetricAttributes);
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
- ChronicleMetrics.constraintViolations.add(totalViolations, {
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
- ChronicleMetrics.appendErrors.add(totalErrors, {
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
- ChronicleMetrics.appendErrors.add(1, {
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', this._telemetry, async span => {
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', this._telemetry, async span => {
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', this._telemetry, async span => {
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', this._telemetry, async span => {
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', this._telemetry, async span => {
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', this._telemetry, async span => {
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', this._telemetry, async span => {
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
- for (const spanNames of ['unknown', '', null, false, 0, {}, [], ['legacy']]) {
11
- describe(`when configuring unsupported span names ${JSON.stringify(spanNames)}`, () => {
12
- const telemetry = { spanNames } as unknown as ChronicleTelemetryOptions;
13
- it('should reject the development options at construction', () => {
14
- (() => ChronicleOptions.development({ telemetry })).should.throw(TypeError, 'telemetry.spanNames must be legacy or convention.');
15
- });
16
- it('should reject the connection string options at construction', () => {
17
- (() => ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { telemetry })).should.throw(TypeError, 'telemetry.spanNames must be legacy or convention.');
18
- });
19
- });
20
- }
21
-
22
- for (const spanNames of [undefined, 'legacy', 'convention'] as const) {
23
- describe(`when configuring supported span names ${spanNames ?? 'by default'}`, () => {
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.42.2",
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,12 +1,13 @@
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 naming and privacy settings; instrumentation itself is always available. */
4
+ /** Per-client privacy and compatibility settings; instrumentation itself is always available. */
5
5
  export interface ChronicleTelemetryOptions {
6
6
  /**
7
- * Span naming convention. Defaults to legacy; both modes use the same scope and attribute families.
8
- * Replacing the earlier "next major" plan, built-in legacy span emission ends in an upcoming
9
- * minor after ADR 0001's one-minor overlap. Both values will remain accepted as no-ops, without a removal deadline.
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.
10
11
  * See https://github.com/Cratis/Chronicle.TypeScript/issues/171.
11
12
  */
12
13
  spanNames?: 'legacy' | 'convention';
@@ -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 unchanged operation name', () => {
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.event_sequences.append');
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 both registries on the same span', () => {
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', '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'
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 expected = spanNames === 'convention' ? conventionSpans : legacySpans;
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'} names`, () => {
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(spanNames === undefined ? undefined : { spanNames });
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 with the selected name', () => {
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(expected[operation]);
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 retain both attribute families', () => {
58
+ it('should emit canonical attributes with only the exact-string sequence number exception', () => {
58
59
  span.attributes.should.include({
59
- 'chronicle.event_store': 'store', 'cratis.event_store.name': 'store',
60
- 'chronicle.namespace': 'namespace', 'cratis.event_store.namespace': 'namespace',
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'} names`, () => {
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(spanNames === undefined ? undefined : { spanNames });
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 with the selected name and shared scope', () => {
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(expected[operation]);
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 both event store attributes', () => {
95
- span.attributes.should.include({ 'chronicle.event_store': 'store', 'cratis.event_store.name': 'store' });
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 interleaving two clients with different span naming modes', () => {
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-store'), convention.getEventStore('convention-store')
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 keep each client and its cached stores in its own mode', () => {
125
- spans.should.have.lengthOf(12);
126
- spans.filter(span => span.attributes['cratis.event_store.name'] === 'legacy-store').map(span => span.name).should.deep.equal([
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 use the same tracer scope in both modes', () => {
135
- new Set(spans.map(span => `${span.instrumentationScope.name}/${span.instrumentationScope.version}`)).should.deep.equal(
136
- new Set([`Cratis.Chronicle.Client/${clientVersion}`]));
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
  });
@@ -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 equally to both names on every operation', () => {
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'], expected);
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.include({ 'cratis.event.count': 2, 'chronicle.events_count': 2 });
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
  });