@cratis/chronicle 6.32.1 → 6.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/ChronicleClient.ts +35 -33
  2. package/ChronicleOptions.ts +27 -1
  3. package/EventStore.ts +14 -12
  4. package/Metrics.ts +70 -96
  5. package/Tracing.ts +4 -2
  6. package/WellKnownTelemetryNames.ts +67 -0
  7. package/artifacts/validateArtifactSchemas.spec.ts +1 -1
  8. package/artifacts/withActivatedArtifact.ts +7 -5
  9. package/connection/ChronicleConnection.ts +18 -6
  10. package/connection/CompatibilityPreflight.ts +2 -1
  11. package/connection/KernelKeepAlive.ts +13 -7
  12. package/connection/TokenProvider.ts +3 -1
  13. package/connection/TraceContextMiddleware.ts +30 -0
  14. package/connection/for_ChronicleConnection/when_creating_clients/with_correlated_multi_host_callers.spec.ts +73 -0
  15. package/connection/for_OAuthTokenProvider/when_acquiring_tokens/with_http_instrumentation.spec.ts +102 -0
  16. package/connection/for_TraceContextMiddleware/when_calling/with_active_context.spec.ts +235 -0
  17. package/connection/for_TraceContextMiddleware/when_calling/with_grpc_instrumentation.spec.ts +102 -0
  18. package/correlation/CorrelationIdManager.ts +10 -0
  19. package/dist/ChronicleClient.d.ts.map +1 -1
  20. package/dist/ChronicleClient.js +35 -33
  21. package/dist/ChronicleClient.js.map +1 -1
  22. package/dist/ChronicleOptions.d.ts +8 -0
  23. package/dist/ChronicleOptions.d.ts.map +1 -1
  24. package/dist/ChronicleOptions.js +19 -1
  25. package/dist/ChronicleOptions.js.map +1 -1
  26. package/dist/EventStore.d.ts +3 -1
  27. package/dist/EventStore.d.ts.map +1 -1
  28. package/dist/EventStore.js +14 -12
  29. package/dist/EventStore.js.map +1 -1
  30. package/dist/Metrics.d.ts +23 -64
  31. package/dist/Metrics.d.ts.map +1 -1
  32. package/dist/Metrics.js +62 -92
  33. package/dist/Metrics.js.map +1 -1
  34. package/dist/Tracing.d.ts +1 -1
  35. package/dist/Tracing.d.ts.map +1 -1
  36. package/dist/Tracing.js +4 -2
  37. package/dist/Tracing.js.map +1 -1
  38. package/dist/WellKnownTelemetryNames.d.ts +65 -0
  39. package/dist/WellKnownTelemetryNames.d.ts.map +1 -0
  40. package/dist/WellKnownTelemetryNames.js +67 -0
  41. package/dist/WellKnownTelemetryNames.js.map +1 -0
  42. package/dist/artifacts/withActivatedArtifact.d.ts +2 -1
  43. package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -1
  44. package/dist/artifacts/withActivatedArtifact.js +5 -4
  45. package/dist/artifacts/withActivatedArtifact.js.map +1 -1
  46. package/dist/connection/ChronicleConnection.d.ts +5 -0
  47. package/dist/connection/ChronicleConnection.d.ts.map +1 -1
  48. package/dist/connection/ChronicleConnection.js +13 -6
  49. package/dist/connection/ChronicleConnection.js.map +1 -1
  50. package/dist/connection/CompatibilityPreflight.d.ts.map +1 -1
  51. package/dist/connection/CompatibilityPreflight.js +2 -1
  52. package/dist/connection/CompatibilityPreflight.js.map +1 -1
  53. package/dist/connection/KernelKeepAlive.d.ts +3 -1
  54. package/dist/connection/KernelKeepAlive.d.ts.map +1 -1
  55. package/dist/connection/KernelKeepAlive.js +10 -7
  56. package/dist/connection/KernelKeepAlive.js.map +1 -1
  57. package/dist/connection/TokenProvider.d.ts.map +1 -1
  58. package/dist/connection/TokenProvider.js +3 -1
  59. package/dist/connection/TokenProvider.js.map +1 -1
  60. package/dist/connection/TraceContextMiddleware.d.ts +4 -0
  61. package/dist/connection/TraceContextMiddleware.d.ts.map +1 -0
  62. package/dist/connection/TraceContextMiddleware.js +30 -0
  63. package/dist/connection/TraceContextMiddleware.js.map +1 -0
  64. package/dist/correlation/CorrelationIdManager.d.ts +4 -0
  65. package/dist/correlation/CorrelationIdManager.d.ts.map +1 -1
  66. package/dist/correlation/CorrelationIdManager.js +8 -0
  67. package/dist/correlation/CorrelationIdManager.js.map +1 -1
  68. package/dist/eventSequences/EventLog.d.ts +2 -1
  69. package/dist/eventSequences/EventLog.d.ts.map +1 -1
  70. package/dist/eventSequences/EventLog.js +2 -2
  71. package/dist/eventSequences/EventLog.js.map +1 -1
  72. package/dist/eventSequences/EventSequence.d.ts +4 -1
  73. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  74. package/dist/eventSequences/EventSequence.js +63 -80
  75. package/dist/eventSequences/EventSequence.js.map +1 -1
  76. package/dist/eventStoreSubscriptions/EventStoreSubscriptions.d.ts.map +1 -1
  77. package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js +3 -4
  78. package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js.map +1 -1
  79. package/dist/index.d.ts +7 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +4 -1
  82. package/dist/index.js.map +1 -1
  83. package/dist/logging/ChronicleLogEntry.d.ts +11 -0
  84. package/dist/logging/ChronicleLogEntry.d.ts.map +1 -0
  85. package/dist/logging/ChronicleLogEntry.js +4 -0
  86. package/dist/logging/ChronicleLogEntry.js.map +1 -0
  87. package/dist/logging/ChronicleLogLevel.d.ts +9 -0
  88. package/dist/logging/ChronicleLogLevel.d.ts.map +1 -0
  89. package/dist/logging/ChronicleLogLevel.js +12 -0
  90. package/dist/logging/ChronicleLogLevel.js.map +1 -0
  91. package/dist/logging/DiagChronicleLogger.d.ts +7 -0
  92. package/dist/logging/DiagChronicleLogger.d.ts.map +1 -0
  93. package/dist/logging/DiagChronicleLogger.js +10 -0
  94. package/dist/logging/DiagChronicleLogger.js.map +1 -0
  95. package/dist/logging/IChronicleLogger.d.ts +7 -0
  96. package/dist/logging/IChronicleLogger.d.ts.map +1 -0
  97. package/dist/logging/IChronicleLogger.js +4 -0
  98. package/dist/logging/IChronicleLogger.js.map +1 -0
  99. package/dist/logging/createLogger.d.ts +10 -0
  100. package/dist/logging/createLogger.d.ts.map +1 -0
  101. package/dist/logging/createLogger.js +55 -0
  102. package/dist/logging/createLogger.js.map +1 -0
  103. package/dist/projections/Projections.d.ts.map +1 -1
  104. package/dist/projections/Projections.js +4 -3
  105. package/dist/projections/Projections.js.map +1 -1
  106. package/dist/reactors/Reactors.d.ts.map +1 -1
  107. package/dist/reactors/Reactors.js +26 -24
  108. package/dist/reactors/Reactors.js.map +1 -1
  109. package/dist/readModels/IReadModels.d.ts +14 -0
  110. package/dist/readModels/IReadModels.d.ts.map +1 -1
  111. package/dist/readModels/ReadModels.d.ts +6 -0
  112. package/dist/readModels/ReadModels.d.ts.map +1 -1
  113. package/dist/readModels/ReadModels.js +110 -0
  114. package/dist/readModels/ReadModels.js.map +1 -1
  115. package/dist/readModels/for_ReadModels/given/a_stored_read_model.d.ts +20 -0
  116. package/dist/readModels/for_ReadModels/given/a_stored_read_model.d.ts.map +1 -0
  117. package/dist/readModels/for_ReadModels/given/a_stored_read_model.js +79 -0
  118. package/dist/readModels/for_ReadModels/given/a_stored_read_model.js.map +1 -0
  119. package/dist/reducers/Reducers.d.ts.map +1 -1
  120. package/dist/reducers/Reducers.js +28 -25
  121. package/dist/reducers/Reducers.js.map +1 -1
  122. package/dist/seeding/EventSeeding.d.ts.map +1 -1
  123. package/dist/seeding/EventSeeding.js +4 -3
  124. package/dist/seeding/EventSeeding.js.map +1 -1
  125. package/dist/telemetry/ChronicleTelemetryOptions.d.ts +11 -0
  126. package/dist/telemetry/ChronicleTelemetryOptions.d.ts.map +1 -0
  127. package/dist/telemetry/ChronicleTelemetryOptions.js +4 -0
  128. package/dist/telemetry/ChronicleTelemetryOptions.js.map +1 -0
  129. package/dist/telemetry/observeOperation.d.ts +6 -0
  130. package/dist/telemetry/observeOperation.d.ts.map +1 -0
  131. package/dist/telemetry/observeOperation.js +23 -0
  132. package/dist/telemetry/observeOperation.js.map +1 -0
  133. package/dist/telemetry/runInBackgroundContext.d.ts +3 -0
  134. package/dist/telemetry/runInBackgroundContext.d.ts.map +1 -0
  135. package/dist/telemetry/runInBackgroundContext.js +9 -0
  136. package/dist/telemetry/runInBackgroundContext.js.map +1 -0
  137. package/dist/telemetry/spanAttributes.d.ts +14 -0
  138. package/dist/telemetry/spanAttributes.d.ts.map +1 -0
  139. package/dist/telemetry/spanAttributes.js +47 -0
  140. package/dist/telemetry/spanAttributes.js.map +1 -0
  141. package/dist/tsconfig.tsbuildinfo +1 -1
  142. package/eventSequences/EventLog.ts +4 -2
  143. package/eventSequences/EventSequence.ts +64 -81
  144. package/eventStoreSubscriptions/EventStoreSubscriptions.ts +5 -5
  145. package/for_ChronicleClient/when_starting_background_work/with_a_correlated_caller.spec.ts +150 -0
  146. package/for_ChronicleOptions/when_configuring_telemetry/with_event_source_identifiers.spec.ts +44 -0
  147. package/index.ts +7 -1
  148. package/logging/ChronicleLogEntry.ts +14 -0
  149. package/logging/ChronicleLogLevel.ts +11 -0
  150. package/logging/DiagChronicleLogger.ts +13 -0
  151. package/logging/IChronicleLogger.ts +10 -0
  152. package/logging/createLogger.ts +50 -0
  153. package/logging/for_ChronicleLogger/when_logging/with_client_owned_sinks.spec.ts +96 -0
  154. package/package.json +7 -1
  155. package/projections/Projections.ts +4 -3
  156. package/reactors/Reactors.ts +27 -25
  157. package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +14 -14
  158. package/readModels/IReadModels.ts +15 -0
  159. package/readModels/ReadModels.ts +118 -1
  160. package/readModels/for_ReadModels/given/a_stored_read_model.ts +62 -0
  161. package/readModels/for_ReadModels/when_releasing_a_document/with_a_default_subject.spec.ts +39 -0
  162. package/readModels/for_ReadModels/when_releasing_a_document/with_a_frozen_document.spec.ts +28 -0
  163. package/readModels/for_ReadModels/when_releasing_a_document/with_a_malformed_response.spec.ts +24 -0
  164. package/readModels/for_ReadModels/when_releasing_a_document/with_a_reported_failure.spec.ts +38 -0
  165. package/readModels/for_ReadModels/when_releasing_a_document/with_an_empty_released_value.spec.ts +19 -0
  166. package/readModels/for_ReadModels/when_releasing_a_document/with_an_unknown_subject_property.spec.ts +42 -0
  167. package/readModels/for_ReadModels/when_releasing_a_document/with_bookkeeping_fields.spec.ts +27 -0
  168. package/readModels/for_ReadModels/when_releasing_a_document/with_conflicting_or_missing_properties.spec.ts +32 -0
  169. package/readModels/for_ReadModels/when_releasing_a_document/with_empty_subject_metadata.spec.ts +56 -0
  170. package/readModels/for_ReadModels/when_releasing_a_document/with_invalid_subject_metadata.spec.ts +27 -0
  171. package/readModels/for_ReadModels/when_releasing_a_document/with_mixed_subjects.spec.ts +38 -0
  172. package/readModels/for_ReadModels/when_releasing_a_document/with_property_subjects.spec.ts +52 -0
  173. package/readModels/for_ReadModels/when_releasing_a_document/with_whitespace_subjects.spec.ts +24 -0
  174. package/readModels/for_ReadModels/when_releasing_a_document/without_subjects.spec.ts +24 -0
  175. package/reducers/Reducers.ts +29 -26
  176. package/seeding/EventSeeding.ts +6 -4
  177. package/telemetry/ChronicleTelemetryOptions.ts +8 -0
  178. package/telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.ts +36 -0
  179. package/telemetry/for_ChronicleTelemetry/given/an_event_sequence.fixture.ts +29 -0
  180. package/telemetry/for_ChronicleTelemetry/when_appending/with_shared_and_legacy_attributes.spec.ts +58 -0
  181. package/telemetry/for_ChronicleTelemetry/when_appending/without_an_sdk.spec.ts +15 -0
  182. package/telemetry/for_ChronicleTelemetry/when_documenting/with_public_names.spec.ts +15 -0
  183. package/telemetry/for_ChronicleTelemetry/when_recording_failures/with_sensitive_exception_details.spec.ts +34 -0
  184. package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts +92 -0
  185. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_legacy_overlap.spec.ts +40 -0
  186. package/telemetry/for_ChronicleTelemetry/when_recording_sequence_numbers/with_integer_precision.spec.ts +23 -0
  187. package/telemetry/observeOperation.ts +25 -0
  188. package/telemetry/runInBackgroundContext.ts +10 -0
  189. package/telemetry/spanAttributes.ts +49 -0
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- import { diag } from '@opentelemetry/api';
4
+ import { createLogger } from '../logging/createLogger.js';
5
5
  import { 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 = diag.createComponentLogger({ namespace: '@cratis/chronicle/EventSeeding' });
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: String(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
+ }
@@ -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
+ });
@@ -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
+ }
@@ -0,0 +1,49 @@
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 { SpanStatusCode, type Span, type AttributeValue } from '@opentelemetry/api';
6
+ import { WellKnownTelemetryNames as names } from '../WellKnownTelemetryNames.js';
7
+ import type { ChronicleTelemetryOptions } from './ChronicleTelemetryOptions.js';
8
+
9
+ /** Records a shared attribute and its legacy alias on the same span. */
10
+ export function setTelemetryAttribute(span: Span, name: keyof typeof names.legacyAttributes, value: AttributeValue): void {
11
+ span.setAttribute(names.legacyAttributes[name], value);
12
+ if (name in names.attributes) {
13
+ span.setAttribute(names.attributes[name as keyof typeof names.attributes], value);
14
+ }
15
+ }
16
+
17
+ /** Preserves the legacy string; records the shared integer only when exactly representable. */
18
+ export function setSequenceNumber(span: Span, value: bigint): void {
19
+ span.setAttribute(names.legacyAttributes.sequenceNumber, value.toString());
20
+ const number = Number(value);
21
+ if (Number.isSafeInteger(number)) span.setAttribute(names.attributes.sequenceNumber, number);
22
+ }
23
+
24
+ /** Applies the same privacy policy to both event source identifier names. */
25
+ export function setEventSourceId(span: Span, value: string | undefined, options?: ChronicleTelemetryOptions): void {
26
+ try {
27
+ const policy = options?.eventSourceId;
28
+ if (value === undefined || !policy || !span.isRecording()) return;
29
+ if (policy.mode !== 'raw' && policy.mode !== 'hmac') return;
30
+ const recorded = policy.mode === 'raw' ? value : createHmac('sha256', policy.key).update(value).digest('hex');
31
+ setTelemetryAttribute(span, 'eventSourceId', recorded);
32
+ } catch {
33
+ // Options are validated at construction, but callers can mutate them later.
34
+ // Optional telemetry enrichment must never prevent an RPC or span completion.
35
+ }
36
+ }
37
+
38
+ /** Classifies failures without serializing arbitrary thrown values, messages or stacks. */
39
+ export function exceptionType(error: unknown): string {
40
+ return error instanceof Error ? error.name : typeof error;
41
+ }
42
+
43
+ /** Records failure type only; preserves the original exception for the caller. */
44
+ export function recordSafeException(span: Span, error: unknown): void {
45
+ const type = exceptionType(error);
46
+ span.setStatus({ code: SpanStatusCode.ERROR });
47
+ span.setAttribute(names.attributes.errorType, type);
48
+ span.addEvent('exception', { [names.attributes.exceptionType]: type });
49
+ }