@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
package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_client_operations.spec.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
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, chai, describe, it, vi } from 'vitest';
|
|
5
|
+
import type { ScopeMetrics } from '@opentelemetry/sdk-metrics';
|
|
6
|
+
import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
|
|
7
|
+
import { client } from '../given/a_client.fixture.js';
|
|
8
|
+
import { eventSequence, Recorded } from '../given/an_event_sequence.fixture.js';
|
|
9
|
+
import { WellKnownTelemetryNames as names } from '../../../WellKnownTelemetryNames.js';
|
|
10
|
+
import { clientVersion } from '../../../connection/clientVersion.js';
|
|
11
|
+
|
|
12
|
+
chai.should();
|
|
13
|
+
const telemetry = telemetrySession();
|
|
14
|
+
describe('when recording metrics from successful client operations', () => {
|
|
15
|
+
let scopes: ScopeMetrics[];
|
|
16
|
+
let scope: ScopeMetrics;
|
|
17
|
+
const sequenceAttributes = {
|
|
18
|
+
'cratis.event_store.name': 'store', 'cratis.event_store.namespace': 'namespace',
|
|
19
|
+
'cratis.event_sequence.id': 'event-log'
|
|
20
|
+
};
|
|
21
|
+
const eventAttributes = { ...sequenceAttributes, 'cratis.event_type.id': 'telemetry-recorded' };
|
|
22
|
+
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
const instance = client();
|
|
25
|
+
const { sequence, services } = eventSequence();
|
|
26
|
+
let milliseconds = 1000;
|
|
27
|
+
const clock = vi.spyOn(performance, 'now').mockImplementation(() => milliseconds);
|
|
28
|
+
services.append.mockImplementation(async () => {
|
|
29
|
+
milliseconds += 250;
|
|
30
|
+
return { Response: { SequenceNumber: 42n } };
|
|
31
|
+
});
|
|
32
|
+
services.appendManyForEventSources.mockImplementation(async () => {
|
|
33
|
+
milliseconds += 1250;
|
|
34
|
+
return { Response: { SequenceNumbers: [43n, 44n] } };
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
await instance.getEventStore('store', 'namespace');
|
|
38
|
+
await sequence.append('private-source', new Recorded());
|
|
39
|
+
await sequence.appendMany('private-source', [new Recorded(), new Recorded()]);
|
|
40
|
+
scopes = (await telemetry.reader.collect()).resourceMetrics.scopeMetrics;
|
|
41
|
+
scope = scopes[0];
|
|
42
|
+
} finally {
|
|
43
|
+
clock.mockRestore();
|
|
44
|
+
instance.dispose();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should use the canonical scope and installed client version', () => {
|
|
49
|
+
scopes.should.have.lengthOf(1);
|
|
50
|
+
scope.scope.should.include({ name: 'Cratis.Chronicle.Client', version: clientVersion });
|
|
51
|
+
});
|
|
52
|
+
it('should emit only the expected canonical instruments with no legacy names', () => {
|
|
53
|
+
scope.metrics.map(metric => metric.descriptor.name).should.have.members([
|
|
54
|
+
names.metrics.eventsAppended, names.metrics.batchAppendsPerformed, names.metrics.eventStoreRetrievals,
|
|
55
|
+
names.metrics.appendDuration, names.metrics.appendManyDuration
|
|
56
|
+
]);
|
|
57
|
+
scope.metrics.filter(metric => metric.descriptor.name.startsWith('chronicle.')).should.be.empty;
|
|
58
|
+
});
|
|
59
|
+
it('should record the single append duration in seconds exactly once', () => {
|
|
60
|
+
const metric = scope.metrics.find(metric => metric.descriptor.name === names.metrics.appendDuration)!;
|
|
61
|
+
metric.descriptor.unit.should.equal('s');
|
|
62
|
+
metric.dataPoints.should.have.lengthOf(1);
|
|
63
|
+
metric.dataPoints[0].attributes.should.deep.equal(eventAttributes);
|
|
64
|
+
metric.dataPoints[0].value.should.include({ min: 0.25, max: 0.25, sum: 0.25, count: 1 });
|
|
65
|
+
});
|
|
66
|
+
it('should record the batch append duration in seconds exactly once', () => {
|
|
67
|
+
const metric = scope.metrics.find(metric => metric.descriptor.name === names.metrics.appendManyDuration)!;
|
|
68
|
+
metric.descriptor.unit.should.equal('s');
|
|
69
|
+
metric.dataPoints.should.have.lengthOf(1);
|
|
70
|
+
metric.dataPoints[0].attributes.should.deep.equal(sequenceAttributes);
|
|
71
|
+
metric.dataPoints[0].value.should.include({ min: 1.25, max: 1.25, sum: 1.25, count: 1 });
|
|
72
|
+
});
|
|
73
|
+
it('should count individual events with the dimensions known to each append path', () => {
|
|
74
|
+
const metric = scope.metrics.find(metric => metric.descriptor.name === names.metrics.eventsAppended)!;
|
|
75
|
+
metric.dataPoints.map(point => ({ attributes: point.attributes, value: point.value })).should.have.deep.members([
|
|
76
|
+
{ attributes: eventAttributes, value: 1 }, { attributes: sequenceAttributes, value: 2 }
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
it('should count the batch without a batch-size dimension', () => {
|
|
80
|
+
const metric = scope.metrics.find(metric => metric.descriptor.name === names.metrics.batchAppendsPerformed)!;
|
|
81
|
+
metric.dataPoints.should.have.lengthOf(1);
|
|
82
|
+
metric.dataPoints[0].attributes.should.deep.equal(sequenceAttributes);
|
|
83
|
+
metric.dataPoints[0].value.should.equal(1);
|
|
84
|
+
});
|
|
85
|
+
it('should count the event-store retrieval with only store and namespace dimensions', () => {
|
|
86
|
+
const metric = scope.metrics.find(metric => metric.descriptor.name === names.metrics.eventStoreRetrievals)!;
|
|
87
|
+
metric.dataPoints.should.have.lengthOf(1);
|
|
88
|
+
metric.dataPoints[0].attributes.should.deep.equal({
|
|
89
|
+
'cratis.event_store.name': 'store', 'cratis.event_store.namespace': 'namespace'
|
|
90
|
+
});
|
|
91
|
+
metric.dataPoints[0].value.should.equal(1);
|
|
92
|
+
});
|
|
93
|
+
it('should exclude legacy, batch-size and sensitive dimensions from every measurement', () => {
|
|
94
|
+
for (const metric of scope.metrics) {
|
|
95
|
+
for (const point of metric.dataPoints) {
|
|
96
|
+
Object.keys(point.attributes).filter(key => ![
|
|
97
|
+
'cratis.event_store.name', 'cratis.event_store.namespace', 'cratis.event_sequence.id', 'cratis.event_type.id'
|
|
98
|
+
].includes(key)).should.be.empty;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
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, chai, describe, it } from 'vitest';
|
|
5
|
+
import type { ScopeMetrics } from '@opentelemetry/sdk-metrics';
|
|
6
|
+
import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
|
|
7
|
+
import { eventSequence, Recorded } from '../given/an_event_sequence.fixture.js';
|
|
8
|
+
import { WellKnownTelemetryNames as names } from '../../../WellKnownTelemetryNames.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
const telemetry = telemetrySession();
|
|
12
|
+
describe('when recording metrics from thrown single and batch append failures', () => {
|
|
13
|
+
let scopes: ScopeMetrics[];
|
|
14
|
+
let scope: ScopeMetrics;
|
|
15
|
+
let outcomes: PromiseSettledResult<unknown>[];
|
|
16
|
+
const failure = new Error('Sensitive transport failure');
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
const { sequence, services } = eventSequence();
|
|
19
|
+
services.append.mockRejectedValue(failure);
|
|
20
|
+
services.appendManyForEventSources.mockRejectedValue(failure);
|
|
21
|
+
outcomes = await Promise.allSettled([
|
|
22
|
+
sequence.append('private-source', new Recorded()),
|
|
23
|
+
sequence.appendMany('private-source', [new Recorded(), new Recorded()])
|
|
24
|
+
]);
|
|
25
|
+
scopes = (await telemetry.reader.collect()).resourceMetrics.scopeMetrics;
|
|
26
|
+
scope = scopes[0];
|
|
27
|
+
});
|
|
28
|
+
it('should preserve both thrown failures', () => {
|
|
29
|
+
outcomes.should.deep.equal([
|
|
30
|
+
{ status: 'rejected', reason: failure }, { status: 'rejected', reason: failure }
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
it('should emit only the canonical error counter and no completed-append durations', () => {
|
|
34
|
+
scopes.should.have.lengthOf(1);
|
|
35
|
+
scope.scope.name.should.equal(names.scope);
|
|
36
|
+
scope.metrics.map(metric => metric.descriptor.name).should.deep.equal([names.metrics.appendErrors]);
|
|
37
|
+
});
|
|
38
|
+
it('should count each failed operation once with only sequence dimensions', () => {
|
|
39
|
+
const metric = scope.metrics[0];
|
|
40
|
+
metric.dataPoints.should.have.lengthOf(1);
|
|
41
|
+
metric.dataPoints[0].attributes.should.deep.equal({
|
|
42
|
+
'cratis.event_store.name': 'store', 'cratis.event_store.namespace': 'namespace',
|
|
43
|
+
'cratis.event_sequence.id': 'event-log'
|
|
44
|
+
});
|
|
45
|
+
metric.dataPoints[0].value.should.equal(2);
|
|
46
|
+
});
|
|
47
|
+
});
|
package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_millisecond_adapters.spec.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
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, chai, describe, it } from 'vitest';
|
|
5
|
+
import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
|
|
6
|
+
import { ChronicleMetrics, ChronicleConventionMetrics } from '../../../Metrics.js';
|
|
7
|
+
import { WellKnownTelemetryNames as names } from '../../../WellKnownTelemetryNames.js';
|
|
8
|
+
import { clientVersion } from '../../../connection/clientVersion.js';
|
|
9
|
+
import type { ScopeMetrics } from '@opentelemetry/sdk-metrics';
|
|
10
|
+
|
|
11
|
+
chai.should();
|
|
12
|
+
const telemetry = telemetrySession();
|
|
13
|
+
describe('when recording through the deprecated millisecond compatibility bridge', () => {
|
|
14
|
+
let scope: ScopeMetrics;
|
|
15
|
+
const attributes = Object.freeze({ 'chronicle.event_store': 'store', 'chronicle.namespace': 'namespace',
|
|
16
|
+
'chronicle.event_sequence_id': 'event-log', 'chronicle.event_type_id': 'recorded',
|
|
17
|
+
'chronicle.events_count': 237, 'chronicle.event_source_id': 'private-source',
|
|
18
|
+
'cratis.correlation_id': 'private-correlation', tenant: 'custom-dimension' });
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
// Metrics was imported before provider installation: recording must still reach the SDK.
|
|
21
|
+
for (const instrument of ['eventsAppended', 'batchAppendsPerformed', 'eventStoreRetrievals', 'constraintViolations', 'appendErrors'] as const) {
|
|
22
|
+
ChronicleMetrics[instrument].add(3, attributes);
|
|
23
|
+
}
|
|
24
|
+
for (const instrument of ['appendDuration', 'appendManyDuration'] as const) {
|
|
25
|
+
ChronicleMetrics[instrument].record(250, attributes);
|
|
26
|
+
// Canonical keys take precedence over historical input keys; direct seconds are not converted.
|
|
27
|
+
ChronicleConventionMetrics[instrument].record(0.5, { 'cratis.event_store.name': 'direct' });
|
|
28
|
+
ChronicleMetrics[instrument].record(1250, { 'chronicle.event_store': 'ignored', 'cratis.event_store.name': 'canonical-input' });
|
|
29
|
+
}
|
|
30
|
+
scope = (await telemetry.reader.collect()).resourceMetrics.scopeMetrics[0];
|
|
31
|
+
});
|
|
32
|
+
it('should use the shared scope and installed version for both families', () => {
|
|
33
|
+
scope.scope.should.include({ name: 'Cratis.Chronicle.Client', version: clientVersion });
|
|
34
|
+
});
|
|
35
|
+
it('should retain dual recording for every compatibility instrument', () => {
|
|
36
|
+
scope.metrics.map(metric => metric.descriptor.name).should.have.members([
|
|
37
|
+
...Object.values(names.metrics), ...Object.values(names.legacyMetrics)
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
for (const instrument of ['appendDuration', 'appendManyDuration'] as const) {
|
|
41
|
+
it(`should retain ${instrument} milliseconds and record seconds exactly once`, () => {
|
|
42
|
+
const legacy = scope.metrics.find(metric => metric.descriptor.name === names.legacyMetrics[instrument])!;
|
|
43
|
+
legacy.descriptor.unit.should.equal('ms');
|
|
44
|
+
legacy.dataPoints.should.have.lengthOf(2);
|
|
45
|
+
legacy.dataPoints.find(point => point.attributes['chronicle.event_store'] === 'store')!.value.should.include({
|
|
46
|
+
min: 250, max: 250, sum: 250, count: 1
|
|
47
|
+
});
|
|
48
|
+
legacy.dataPoints.find(point => point.attributes['chronicle.event_store'] === 'ignored')!.value.should.include({
|
|
49
|
+
min: 1250, max: 1250, sum: 1250, count: 1
|
|
50
|
+
});
|
|
51
|
+
const canonical = scope.metrics.find(metric => metric.descriptor.name === names.metrics[instrument])!;
|
|
52
|
+
canonical.descriptor.unit.should.equal('s');
|
|
53
|
+
canonical.dataPoints.should.have.lengthOf(3);
|
|
54
|
+
canonical.dataPoints.find(point => point.attributes['cratis.event_store.name'] === 'store')!.value.should.include({
|
|
55
|
+
min: 0.25, max: 0.25, sum: 0.25, count: 1
|
|
56
|
+
});
|
|
57
|
+
canonical.dataPoints.find(point => point.attributes['cratis.event_store.name'] === 'canonical-input')!.value.should.include({
|
|
58
|
+
min: 1.25, max: 1.25, sum: 1.25, count: 1
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
it(`should leave direct ${instrument} seconds unchanged without emitting a legacy measurement`, () => {
|
|
62
|
+
const metric = scope.metrics.find(metric => metric.descriptor.name === names.metrics[instrument])!;
|
|
63
|
+
metric.dataPoints.find(point => point.attributes['cratis.event_store.name'] === 'direct')!.value.should.include({
|
|
64
|
+
min: 0.5, max: 0.5, sum: 0.5, count: 1
|
|
65
|
+
});
|
|
66
|
+
const legacy = scope.metrics.find(metric => metric.descriptor.name === names.legacyMetrics[instrument])!;
|
|
67
|
+
legacy.dataPoints.filter(point => point.attributes['cratis.event_store.name'] === 'direct').should.be.empty;
|
|
68
|
+
});
|
|
69
|
+
it(`should retain millisecond and second bucket boundaries for ${instrument}`, () => {
|
|
70
|
+
const milliseconds = [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000];
|
|
71
|
+
for (const [name, boundaries] of [
|
|
72
|
+
[names.legacyMetrics[instrument], milliseconds],
|
|
73
|
+
[names.metrics[instrument], milliseconds.map(value => value / 1000)]
|
|
74
|
+
] as const) {
|
|
75
|
+
const metric = scope.metrics.find(metric => metric.descriptor.name === name)!;
|
|
76
|
+
metric.dataPoints[0].value.should.have.nested.property('buckets.boundaries').deep.equal(boundaries);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
it('should preserve counter values in both families without scaling or duplication', () => {
|
|
81
|
+
for (const metric of scope.metrics.filter(metric => !metric.descriptor.name.endsWith('_duration'))) {
|
|
82
|
+
metric.dataPoints.should.have.lengthOf(1);
|
|
83
|
+
metric.dataPoints[0].value.should.equal(3);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
it('should preserve every caller attribute on the legacy measurements', () => {
|
|
87
|
+
for (const metric of scope.metrics.filter(metric => metric.descriptor.name.startsWith('chronicle.'))) {
|
|
88
|
+
metric.dataPoints.find(point => point.attributes['chronicle.event_store'] === 'store')!.attributes.should.deep.equal(attributes);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
it('should keep canonical dimensions bounded without forwarding custom or sensitive dimensions', () => {
|
|
92
|
+
for (const metric of scope.metrics.filter(metric => metric.descriptor.name.startsWith('cratis.'))) {
|
|
93
|
+
metric.dataPoints.find(point => point.attributes['cratis.event_store.name'] === 'store')!.attributes.should.deep.equal({
|
|
94
|
+
'cratis.event_store.name': 'store', 'cratis.event_store.namespace': 'namespace',
|
|
95
|
+
'cratis.event_sequence.id': 'event-log', 'cratis.event_type.id': 'recorded'
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
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, chai, describe, it, vi } from 'vitest';
|
|
5
|
+
import { ConstraintType } from '@cratis/chronicle.contracts';
|
|
6
|
+
import type { ScopeMetrics } from '@opentelemetry/sdk-metrics';
|
|
7
|
+
import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
|
|
8
|
+
import { eventSequence, Recorded } from '../given/an_event_sequence.fixture.js';
|
|
9
|
+
import { WellKnownTelemetryNames as names } from '../../../WellKnownTelemetryNames.js';
|
|
10
|
+
|
|
11
|
+
chai.should();
|
|
12
|
+
const telemetry = telemetrySession();
|
|
13
|
+
describe('when recording metrics from rejected single and batch appends', () => {
|
|
14
|
+
let scopes: ScopeMetrics[];
|
|
15
|
+
let scope: ScopeMetrics;
|
|
16
|
+
const sequenceAttributes = {
|
|
17
|
+
'cratis.event_store.name': 'store', 'cratis.event_store.namespace': 'namespace',
|
|
18
|
+
'cratis.event_sequence.id': 'event-log'
|
|
19
|
+
};
|
|
20
|
+
const eventAttributes = { ...sequenceAttributes, 'cratis.event_type.id': 'telemetry-recorded' };
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
const { sequence, services } = eventSequence();
|
|
24
|
+
const rejection = {
|
|
25
|
+
ConstraintViolations: [{
|
|
26
|
+
EventTypeId: 'telemetry-recorded', SequenceNumber: 0n, ConstraintType: ConstraintType.Unique,
|
|
27
|
+
ConstraintName: 'unique', Message: 'Value must be unique', Details: {}
|
|
28
|
+
}],
|
|
29
|
+
Errors: ['Append rejected']
|
|
30
|
+
};
|
|
31
|
+
let milliseconds = 1000;
|
|
32
|
+
const clock = vi.spyOn(performance, 'now').mockImplementation(() => milliseconds);
|
|
33
|
+
services.append.mockImplementation(async () => {
|
|
34
|
+
milliseconds += 500;
|
|
35
|
+
return { Response: { SequenceNumber: 0n, ...rejection } };
|
|
36
|
+
});
|
|
37
|
+
services.appendManyForEventSources.mockImplementation(async () => {
|
|
38
|
+
milliseconds += 2500;
|
|
39
|
+
return { Response: { SequenceNumbers: [], ...rejection } };
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
await sequence.append('private-source', new Recorded());
|
|
43
|
+
await sequence.appendMany('private-source', [new Recorded(), new Recorded()]);
|
|
44
|
+
scopes = (await telemetry.reader.collect()).resourceMetrics.scopeMetrics;
|
|
45
|
+
scope = scopes[0];
|
|
46
|
+
} finally {
|
|
47
|
+
clock.mockRestore();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should emit only canonical append instruments', () => {
|
|
52
|
+
scopes.should.have.lengthOf(1);
|
|
53
|
+
scope.scope.name.should.equal(names.scope);
|
|
54
|
+
scope.metrics.map(metric => metric.descriptor.name).should.have.members([
|
|
55
|
+
names.metrics.eventsAppended, names.metrics.batchAppendsPerformed, names.metrics.appendDuration,
|
|
56
|
+
names.metrics.appendManyDuration, names.metrics.constraintViolations, names.metrics.appendErrors
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
it('should record completed rejection durations in seconds without scaling twice', () => {
|
|
60
|
+
for (const [instrument, seconds, attributes] of [
|
|
61
|
+
['appendDuration', 0.5, eventAttributes], ['appendManyDuration', 2.5, sequenceAttributes]
|
|
62
|
+
] as const) {
|
|
63
|
+
const metric = scope.metrics.find(metric => metric.descriptor.name === names.metrics[instrument])!;
|
|
64
|
+
metric.descriptor.unit.should.equal('s');
|
|
65
|
+
metric.dataPoints.should.have.lengthOf(1);
|
|
66
|
+
metric.dataPoints[0].attributes.should.deep.equal(attributes);
|
|
67
|
+
metric.dataPoints[0].value.should.include({ min: seconds, max: seconds, sum: seconds, count: 1 });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
it('should retain per-result violation and error counts with only sequence dimensions', () => {
|
|
71
|
+
for (const instrument of ['constraintViolations', 'appendErrors'] as const) {
|
|
72
|
+
const metric = scope.metrics.find(metric => metric.descriptor.name === names.metrics[instrument])!;
|
|
73
|
+
metric.dataPoints.should.have.lengthOf(1);
|
|
74
|
+
metric.dataPoints[0].attributes.should.deep.equal(sequenceAttributes);
|
|
75
|
+
metric.dataPoints[0].value.should.equal(3);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
it('should retain completed-append counter semantics for returned rejections', () => {
|
|
79
|
+
const appended = scope.metrics.find(metric => metric.descriptor.name === names.metrics.eventsAppended)!;
|
|
80
|
+
appended.dataPoints.map(point => ({ attributes: point.attributes, value: point.value })).should.have.deep.members([
|
|
81
|
+
{ attributes: eventAttributes, value: 1 }, { attributes: sequenceAttributes, value: 2 }
|
|
82
|
+
]);
|
|
83
|
+
const batches = scope.metrics.find(metric => metric.descriptor.name === names.metrics.batchAppendsPerformed)!;
|
|
84
|
+
batches.dataPoints.should.have.lengthOf(1);
|
|
85
|
+
batches.dataPoints[0].attributes.should.deep.equal(sequenceAttributes);
|
|
86
|
+
batches.dataPoints[0].value.should.equal(1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { afterEach, beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import type { Attributes } from '@opentelemetry/api';
|
|
6
|
+
import type { ChronicleClient } from '../../../ChronicleClient.js';
|
|
7
|
+
import { client } from '../given/a_client.fixture.js';
|
|
8
|
+
import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
const telemetry = telemetrySession();
|
|
12
|
+
|
|
13
|
+
for (const hasEvents of [true, false]) {
|
|
14
|
+
describe(`when recording event presence ${hasEvents}`, () => {
|
|
15
|
+
let instance: ChronicleClient;
|
|
16
|
+
let attributes: Attributes;
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
instance = client();
|
|
19
|
+
const store = await instance.getEventStore('store');
|
|
20
|
+
telemetry.spans.reset();
|
|
21
|
+
await store.eventLog.hasEventsFor(hasEvents ? 'source' : 'empty');
|
|
22
|
+
attributes = telemetry.spans.getFinishedSpans()[0].attributes;
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => instance.dispose());
|
|
25
|
+
it('should record the boolean under only the product name', () => {
|
|
26
|
+
attributes.should.have.property('cratis.chronicle.event_sequence.has_events', hasEvents);
|
|
27
|
+
attributes.should.not.have.property('chronicle.has_events');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('when completing a stream', () => {
|
|
33
|
+
let instance: ChronicleClient;
|
|
34
|
+
let attributes: Attributes;
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
instance = client();
|
|
37
|
+
const store = await instance.getEventStore('store');
|
|
38
|
+
telemetry.spans.reset();
|
|
39
|
+
await store.eventLog.completeStream('orders', 'stream-1');
|
|
40
|
+
attributes = telemetry.spans.getFinishedSpans()[0].attributes;
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => instance.dispose());
|
|
43
|
+
it('should record the stream type under only the product name', () => {
|
|
44
|
+
attributes.should.have.property('cratis.chronicle.event_stream.type', 'orders');
|
|
45
|
+
attributes.should.not.have.property('chronicle.event_stream_type');
|
|
46
|
+
});
|
|
47
|
+
it('should record the stream identifier under only the product name', () => {
|
|
48
|
+
attributes.should.have.property('cratis.chronicle.event_stream.id', 'stream-1');
|
|
49
|
+
attributes.should.not.have.property('chronicle.event_stream_id');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -14,7 +14,7 @@ for (const [number, expected] of [[42n, 42], [9007199254740991n, Number.MAX_SAFE
|
|
|
14
14
|
services.tailSequenceNumber.mockResolvedValue({ IsAuthorized: true, Data: { SequenceNumber: number } });
|
|
15
15
|
await sequence.getTailSequenceNumber();
|
|
16
16
|
});
|
|
17
|
-
it('should
|
|
17
|
+
it('should emit only safe canonical integers and preserve every exact compatibility string', () => {
|
|
18
18
|
const attributes = telemetry.spans.getFinishedSpans()[0].attributes;
|
|
19
19
|
should.equal(attributes['cratis.event_sequence.number'], expected);
|
|
20
20
|
should.equal(attributes['chronicle.sequence_number'], number.toString());
|
|
@@ -6,17 +6,14 @@ import { ChronicleTracer } from '../Tracing.js';
|
|
|
6
6
|
import { correlationIdManager } from '../correlation/index.js';
|
|
7
7
|
import { CorrelationId } from '../correlation/CorrelationId.js';
|
|
8
8
|
import { WellKnownTelemetryNames } from '../WellKnownTelemetryNames.js';
|
|
9
|
-
import type { ChronicleTelemetryOptions } from './ChronicleTelemetryOptions.js';
|
|
10
9
|
|
|
11
10
|
/** Opens one client span, scoping the resolved business correlation without generating a new one. */
|
|
12
11
|
export function observeOperation<T>(
|
|
13
12
|
operation: keyof typeof WellKnownTelemetryNames.conventionSpans,
|
|
14
|
-
telemetry: ChronicleTelemetryOptions | undefined,
|
|
15
13
|
action: (span: Span) => Promise<T>,
|
|
16
14
|
correlationId?: { toString(): string }
|
|
17
15
|
): Promise<T> {
|
|
18
|
-
const
|
|
19
|
-
const name = names[operation];
|
|
16
|
+
const name = WellKnownTelemetryNames.conventionSpans[operation];
|
|
20
17
|
const resolved = correlationId ? new CorrelationId(correlationId.toString()) : correlationIdManager.scoped;
|
|
21
18
|
const run = () => {
|
|
22
19
|
const active = context.active();
|
|
@@ -6,22 +6,19 @@ import { SpanStatusCode, type Span, type AttributeValue } from '@opentelemetry/a
|
|
|
6
6
|
import { WellKnownTelemetryNames as names } from '../WellKnownTelemetryNames.js';
|
|
7
7
|
import type { ChronicleTelemetryOptions } from './ChronicleTelemetryOptions.js';
|
|
8
8
|
|
|
9
|
-
/** Records a shared
|
|
10
|
-
export function setTelemetryAttribute(span: Span, name: keyof typeof names.
|
|
11
|
-
span.setAttribute(names.
|
|
12
|
-
if (name in names.attributes) {
|
|
13
|
-
span.setAttribute(names.attributes[name as keyof typeof names.attributes], value);
|
|
14
|
-
}
|
|
9
|
+
/** Records a canonical shared or product-specific attribute. */
|
|
10
|
+
export function setTelemetryAttribute(span: Span, name: keyof typeof names.attributes, value: AttributeValue): void {
|
|
11
|
+
span.setAttribute(names.attributes[name], value);
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
/** Preserves the
|
|
14
|
+
/** Preserves the exact compatibility string; records the canonical integer only when exactly representable. */
|
|
18
15
|
export function setSequenceNumber(span: Span, value: bigint): void {
|
|
19
16
|
span.setAttribute(names.legacyAttributes.sequenceNumber, value.toString());
|
|
20
17
|
const number = Number(value);
|
|
21
18
|
if (Number.isSafeInteger(number)) span.setAttribute(names.attributes.sequenceNumber, number);
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
/**
|
|
21
|
+
/** Records an event source identifier only under the client's explicit privacy policy. */
|
|
25
22
|
export function setEventSourceId(span: Span, value: string | undefined, options?: ChronicleTelemetryOptions): void {
|
|
26
23
|
try {
|
|
27
24
|
const policy = options?.eventSourceId;
|
package/telemetry/for_ChronicleTelemetry/when_exposing_names/with_legacy_compatibility.spec.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
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 { WellKnownTelemetryNames as names } from '../../../WellKnownTelemetryNames.js';
|
|
6
|
-
import { legacySpans, conventionSpans } from '../given/a_span_name_registry.fixture.js';
|
|
7
|
-
|
|
8
|
-
chai.should();
|
|
9
|
-
describe('when exposing names during the migration overlap', () => {
|
|
10
|
-
it('should preserve every legacy span constant', () => {
|
|
11
|
-
names.spans.should.deep.equal(legacySpans);
|
|
12
|
-
});
|
|
13
|
-
it('should expose a matching convention span for every operation', () => {
|
|
14
|
-
names.conventionSpans.should.deep.equal(conventionSpans);
|
|
15
|
-
Object.keys(names.conventionSpans).should.deep.equal(Object.keys(names.spans));
|
|
16
|
-
});
|
|
17
|
-
it('should preserve the current and historical scopes', () => {
|
|
18
|
-
names.scope.should.equal('Cratis.Chronicle.Client');
|
|
19
|
-
names.legacyScope.should.equal('@cratis/chronicle');
|
|
20
|
-
});
|
|
21
|
-
it('should preserve every legacy attribute constant', () => {
|
|
22
|
-
names.legacyAttributes.should.deep.equal({
|
|
23
|
-
eventStore: 'chronicle.event_store', namespace: 'chronicle.namespace',
|
|
24
|
-
eventSequenceId: 'chronicle.event_sequence_id', sequenceNumber: 'chronicle.sequence_number',
|
|
25
|
-
eventTypeId: 'chronicle.event_type_id', eventTypeGeneration: 'chronicle.event_type_generation',
|
|
26
|
-
eventSourceId: 'chronicle.event_source_id', eventCount: 'chronicle.events_count',
|
|
27
|
-
hasEvents: 'chronicle.has_events', eventStreamType: 'chronicle.event_stream_type', eventStreamId: 'chronicle.event_stream_id'
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
it('should preserve every legacy metric constant', () => {
|
|
31
|
-
names.legacyMetrics.should.deep.equal({
|
|
32
|
-
eventsAppended: 'chronicle.events.appended', batchAppendsPerformed: 'chronicle.events.batch_appends',
|
|
33
|
-
eventStoreRetrievals: 'chronicle.client.event_store_retrievals', appendDuration: 'chronicle.events.append_duration',
|
|
34
|
-
appendManyDuration: 'chronicle.events.append_many_duration', constraintViolations: 'chronicle.events.constraint_violations',
|
|
35
|
-
appendErrors: 'chronicle.events.append_errors'
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
it('should expose the product-only attributes alongside the shared registry', () => {
|
|
39
|
-
names.attributes.should.include({
|
|
40
|
-
hasEvents: 'cratis.chronicle.event_sequence.has_events',
|
|
41
|
-
eventStreamType: 'cratis.chronicle.event_stream.type', eventStreamId: 'cratis.chronicle.event_stream.id'
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1,40 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
-
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
-
|
|
4
|
-
import { afterEach, beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
-
import type { Attributes } from '@opentelemetry/api';
|
|
6
|
-
import type { ChronicleClient } from '../../../ChronicleClient.js';
|
|
7
|
-
import { client } from '../given/a_client.fixture.js';
|
|
8
|
-
import { telemetrySession } from '../given/a_telemetry_session.fixture.js';
|
|
9
|
-
|
|
10
|
-
chai.should();
|
|
11
|
-
const telemetry = telemetrySession();
|
|
12
|
-
|
|
13
|
-
for (const spanNames of ['legacy', 'convention'] as const) {
|
|
14
|
-
for (const hasEvents of [true, false]) {
|
|
15
|
-
describe(`when recording event presence ${hasEvents} with ${spanNames} span names`, () => {
|
|
16
|
-
let instance: ChronicleClient;
|
|
17
|
-
let attributes: Attributes;
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
instance = client({ spanNames });
|
|
20
|
-
const store = await instance.getEventStore('store');
|
|
21
|
-
telemetry.spans.reset();
|
|
22
|
-
await store.eventLog.hasEventsFor(hasEvents ? 'source' : 'empty');
|
|
23
|
-
attributes = telemetry.spans.getFinishedSpans()[0].attributes;
|
|
24
|
-
});
|
|
25
|
-
afterEach(() => instance.dispose());
|
|
26
|
-
it('should record the boolean under both the legacy and product-only names', () => {
|
|
27
|
-
attributes.should.include({
|
|
28
|
-
'chronicle.has_events': hasEvents,
|
|
29
|
-
'cratis.chronicle.event_sequence.has_events': hasEvents
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
describe(`when completing a stream with ${spanNames} span names`, () => {
|
|
35
|
-
let instance: ChronicleClient;
|
|
36
|
-
let attributes: Attributes;
|
|
37
|
-
beforeEach(async () => {
|
|
38
|
-
instance = client({ spanNames });
|
|
39
|
-
const store = await instance.getEventStore('store');
|
|
40
|
-
telemetry.spans.reset();
|
|
41
|
-
await store.eventLog.completeStream('orders', 'stream-1');
|
|
42
|
-
attributes = telemetry.spans.getFinishedSpans()[0].attributes;
|
|
43
|
-
});
|
|
44
|
-
afterEach(() => instance.dispose());
|
|
45
|
-
it('should record the stream type under both names', () => {
|
|
46
|
-
attributes.should.include({ 'chronicle.event_stream_type': 'orders', 'cratis.chronicle.event_stream.type': 'orders' });
|
|
47
|
-
});
|
|
48
|
-
it('should record the stream identifier under both names', () => {
|
|
49
|
-
attributes.should.include({ 'chronicle.event_stream_id': 'stream-1', 'cratis.chronicle.event_stream.id': 'stream-1' });
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
}
|