@wardx/core 0.2.2 → 0.5.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/README.md +18 -2
- package/package.json +2 -2
- package/src/WardxCore.js +25 -1
- package/src/frame/FrameBuilder.js +17 -1
- package/src/index.d.ts +37 -1
- package/src/index.js +9 -0
- package/src/metrics/HyperLogLog.js +106 -0
- package/src/metrics/MetricsRegistry.js +32 -2
- package/src/trace/wrap.js +11 -0
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ The defaults live in `defaults.json`. Do not omit a required key. The loader doe
|
|
|
61
61
|
|
|
62
62
|
**When:** You write a runtime that is not Node.js, or you test the engine without HTTP.
|
|
63
63
|
|
|
64
|
-
**Objective:** Record counters, gauges, histograms, and timers. Then make a frame.
|
|
64
|
+
**Objective:** Record counters, gauges, histograms, distinct estimates, and timers. Then make a frame.
|
|
65
65
|
|
|
66
66
|
```js
|
|
67
67
|
import { WardxCore, loadSdkDefaults } from '@wardx/core';
|
|
@@ -83,6 +83,7 @@ core.counter('match.completed', { mode: 'ranked' }).inc();
|
|
|
83
83
|
core.counter('coins.awarded').add(25);
|
|
84
84
|
core.gauge('players.online').set(12921);
|
|
85
85
|
core.histogram('request.duration').observe(42);
|
|
86
|
+
core.distinct('shot.traffic.hids', { result: 'violating' }).add(hid);
|
|
86
87
|
|
|
87
88
|
const endTimer = core.timer('matchmaking.duration');
|
|
88
89
|
endTimer({ result: 'success' });
|
|
@@ -96,7 +97,7 @@ const frames = core.takePendingFrames();
|
|
|
96
97
|
1. Load the SDK defaults.
|
|
97
98
|
2. Add the identity fields.
|
|
98
99
|
3. Construct `WardxCore`.
|
|
99
|
-
4. Call `counter`, `gauge`, `histogram`, or `timer`.
|
|
100
|
+
4. Call `counter`, `gauge`, `histogram`, `distinct`, or `timer`.
|
|
100
101
|
5. Call `snapshotFrame` when you need a frame.
|
|
101
102
|
6. Call `takePendingFrames` to get the pending frames.
|
|
102
103
|
|
|
@@ -115,6 +116,12 @@ core.histogram('coins.award_size').observe(80, { grantId: 'g-80' });
|
|
|
115
116
|
|
|
116
117
|
`timer(name, dims)` starts a timer. The returned function records the duration in milliseconds into a histogram. You can add dimensions when you stop the timer.
|
|
117
118
|
|
|
119
|
+
`distinct(name, dims).add(identifier)` updates a fixed HyperLogLog sketch. The
|
|
120
|
+
engine hashes the identifier as `SHA-256(privacySalt || 0x00 || identifier)` and
|
|
121
|
+
discards it immediately; the frame contains only 512 HLL registers (`p=9`,
|
|
122
|
+
about 4.6% standard error). Keep `privacySalt` stable across workers and time
|
|
123
|
+
windows so equal identifiers map to equal registers.
|
|
124
|
+
|
|
118
125
|
If a series is above `maxSeriesPerMetric`, or a dimension is not valid, the engine returns a no-op object. The engine increments `wardx.internal.cardinality_dropped`.
|
|
119
126
|
|
|
120
127
|
A dimension value must be a string, a number, or a boolean.
|
|
@@ -295,3 +302,12 @@ Internal series use the prefix `wardx.internal.`.
|
|
|
295
302
|
- Ingest server: `@wardx/server`
|
|
296
303
|
|
|
297
304
|
The wire contract is protocol version 1. A runtime sends `POST /v1/sync` with JSON and gzip. The request header is `X-Wardx-Key`.
|
|
305
|
+
|
|
306
|
+
## Retention activity
|
|
307
|
+
|
|
308
|
+
`WardxCore.retentionActivity(userId)` requires an explicit nonblank user ID and
|
|
309
|
+
emits salted `retention.activity` evidence into the existing event buffer. The
|
|
310
|
+
server persists UTC cohorts and exact received-user D1/D7/D30 returns; the core
|
|
311
|
+
does not infer activity or keep a durable client identity/outbox. Use the same
|
|
312
|
+
user ID and privacy salt across sessions and clients. See the
|
|
313
|
+
[Node retention contract](../node/README.md#user-retention-d1--d7--d30).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wardx/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Runtime-agnostic Wardx engine for metrics, events, logs, Remote Config, and experiments.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wardx",
|
|
@@ -41,4 +41,4 @@
|
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public"
|
|
43
43
|
}
|
|
44
|
-
}
|
|
44
|
+
}
|
package/src/WardxCore.js
CHANGED
|
@@ -3,14 +3,16 @@ import { EventBuffer } from './buffers/EventBuffer.js';
|
|
|
3
3
|
import { LogBuffer } from './buffers/LogBuffer.js';
|
|
4
4
|
import { ConfigStore } from './config/ConfigStore.js';
|
|
5
5
|
import { ExperimentResolver } from './config/ExperimentResolver.js';
|
|
6
|
+
import { subjectHash } from './config/hash.js';
|
|
6
7
|
import { FrameBuilder } from './frame/FrameBuilder.js';
|
|
7
8
|
import { InternalMetrics } from './internal/InternalMetrics.js';
|
|
8
9
|
import { NOOP_COUNTER } from './metrics/Counter.js';
|
|
9
10
|
import { NOOP_GAUGE } from './metrics/Gauge.js';
|
|
10
11
|
import { NOOP_HISTOGRAM } from './metrics/Histogram.js';
|
|
12
|
+
import { NOOP_DISTINCT } from './metrics/HyperLogLog.js';
|
|
11
13
|
import { startTimer } from './metrics/Timer.js';
|
|
12
14
|
import { emit } from './trace/emit.js';
|
|
13
|
-
import { wrapCounter, wrapGauge, wrapHistogram } from './trace/wrap.js';
|
|
15
|
+
import { wrapCounter, wrapDistinct, wrapGauge, wrapHistogram } from './trace/wrap.js';
|
|
14
16
|
|
|
15
17
|
export class WardxCore {
|
|
16
18
|
constructor(settings) {
|
|
@@ -24,6 +26,7 @@ export class WardxCore {
|
|
|
24
26
|
maxDimensionKeys: settings.maxDimensionKeys,
|
|
25
27
|
maxDimensionValueLength: settings.maxDimensionValueLength,
|
|
26
28
|
defaultHistogramBuckets: settings.histogramBuckets,
|
|
29
|
+
privacySalt: settings.privacySalt,
|
|
27
30
|
onCardinalityDropped: () => {
|
|
28
31
|
this.internal.cardinalityDropped += 1;
|
|
29
32
|
}
|
|
@@ -68,6 +71,12 @@ export class WardxCore {
|
|
|
68
71
|
);
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
distinct(name, dims) {
|
|
75
|
+
return this._wrap(this.metrics.distinct(name, dims), NOOP_DISTINCT, (series, noop) =>
|
|
76
|
+
wrapDistinct(this._tracer, series, noop, name, dims)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
timer(name, dims) {
|
|
72
81
|
if (this._tracer === null) return this.metrics.timer(name, dims);
|
|
73
82
|
return startTimer((duration, endDims) => {
|
|
@@ -98,6 +107,20 @@ export class WardxCore {
|
|
|
98
107
|
return wrapped;
|
|
99
108
|
}
|
|
100
109
|
|
|
110
|
+
retentionActivity(userId) {
|
|
111
|
+
if (typeof userId !== 'string' || userId.trim().length === 0) {
|
|
112
|
+
throw new Error('retentionActivity requires a non-empty userId');
|
|
113
|
+
}
|
|
114
|
+
const salt = this.settings.privacySalt;
|
|
115
|
+
if (typeof salt !== 'string' || salt.trim().length === 0) {
|
|
116
|
+
throw new Error('retentionActivity requires a non-empty privacySalt');
|
|
117
|
+
}
|
|
118
|
+
this.event('retention.activity', {
|
|
119
|
+
subject: subjectHash(salt, userId),
|
|
120
|
+
salt: subjectHash(salt, 'wardx.retention.identity')
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
101
124
|
identify(subjectId) {
|
|
102
125
|
if (subjectId === undefined || subjectId === null) {
|
|
103
126
|
this._subjectId = null;
|
|
@@ -202,6 +225,7 @@ export class WardxCore {
|
|
|
202
225
|
counters: physical.metrics.counters.length,
|
|
203
226
|
gauges: physical.metrics.gauges.length,
|
|
204
227
|
histograms: physical.metrics.histograms.length,
|
|
228
|
+
distincts: physical.metrics.distincts?.length || 0,
|
|
205
229
|
events: physical.events.length,
|
|
206
230
|
logs: physical.logs.length,
|
|
207
231
|
droppedLogs: i === 0 ? batch.droppedLogs : 0,
|
|
@@ -21,6 +21,7 @@ function rowCount(frame) {
|
|
|
21
21
|
frame.metrics.counters.length +
|
|
22
22
|
frame.metrics.gauges.length +
|
|
23
23
|
frame.metrics.histograms.length +
|
|
24
|
+
(frame.metrics.distincts?.length || 0) +
|
|
24
25
|
frame.events.length +
|
|
25
26
|
frame.logs.length
|
|
26
27
|
);
|
|
@@ -39,7 +40,8 @@ export class FrameBuilder {
|
|
|
39
40
|
metrics: {
|
|
40
41
|
counters,
|
|
41
42
|
gauges,
|
|
42
|
-
histograms
|
|
43
|
+
histograms,
|
|
44
|
+
...(metrics.distincts?.length > 0 ? { distincts: metrics.distincts.slice() } : {})
|
|
43
45
|
},
|
|
44
46
|
events,
|
|
45
47
|
logs
|
|
@@ -81,6 +83,7 @@ export class FrameBuilder {
|
|
|
81
83
|
counters: 0,
|
|
82
84
|
gauges: 0,
|
|
83
85
|
histograms: 0,
|
|
86
|
+
distincts: 0,
|
|
84
87
|
events: 0,
|
|
85
88
|
logs: 0
|
|
86
89
|
};
|
|
@@ -100,11 +103,17 @@ export class FrameBuilder {
|
|
|
100
103
|
collection(current).push(row);
|
|
101
104
|
if (measure(current).bytes <= maxFrameBytes) return true;
|
|
102
105
|
collection(current).pop();
|
|
106
|
+
if (collection.kind === 'distincts' && current.metrics.distincts.length === 0) {
|
|
107
|
+
delete current.metrics.distincts;
|
|
108
|
+
}
|
|
103
109
|
if (rowCount(current) > 0) {
|
|
104
110
|
finishCurrent();
|
|
105
111
|
collection(current).push(row);
|
|
106
112
|
if (measure(current).bytes <= maxFrameBytes) return true;
|
|
107
113
|
collection(current).pop();
|
|
114
|
+
if (collection.kind === 'distincts' && current.metrics.distincts.length === 0) {
|
|
115
|
+
delete current.metrics.distincts;
|
|
116
|
+
}
|
|
108
117
|
}
|
|
109
118
|
if (countDrop) dropped[collection.kind] += 1;
|
|
110
119
|
return false;
|
|
@@ -116,6 +125,11 @@ export class FrameBuilder {
|
|
|
116
125
|
gauges.kind = 'gauges';
|
|
117
126
|
const histograms = (candidate) => candidate.metrics.histograms;
|
|
118
127
|
histograms.kind = 'histograms';
|
|
128
|
+
const distincts = (candidate) => {
|
|
129
|
+
if (!candidate.metrics.distincts) candidate.metrics.distincts = [];
|
|
130
|
+
return candidate.metrics.distincts;
|
|
131
|
+
};
|
|
132
|
+
distincts.kind = 'distincts';
|
|
119
133
|
const events = (candidate) => candidate.events;
|
|
120
134
|
events.kind = 'events';
|
|
121
135
|
const logs = (candidate) => candidate.logs;
|
|
@@ -124,6 +138,7 @@ export class FrameBuilder {
|
|
|
124
138
|
for (const row of frame.metrics.counters) addRow(counters, row);
|
|
125
139
|
for (const row of frame.metrics.gauges) addRow(gauges, row);
|
|
126
140
|
for (const row of frame.metrics.histograms) addRow(histograms, row);
|
|
141
|
+
for (const row of frame.metrics.distincts || []) addRow(distincts, row);
|
|
127
142
|
for (const row of frame.events) addRow(events, row);
|
|
128
143
|
for (const row of frame.logs) addRow(logs, row);
|
|
129
144
|
|
|
@@ -143,6 +158,7 @@ export class FrameBuilder {
|
|
|
143
158
|
droppedCounters: dropped.counters,
|
|
144
159
|
droppedGauges: dropped.gauges,
|
|
145
160
|
droppedHistograms: dropped.histograms,
|
|
161
|
+
droppedDistincts: dropped.distincts,
|
|
146
162
|
droppedEvents: dropped.events,
|
|
147
163
|
droppedLogs: dropped.logs
|
|
148
164
|
};
|
package/src/index.d.ts
CHANGED
|
@@ -44,6 +44,10 @@ export interface HistogramHandle {
|
|
|
44
44
|
observe(value: number, attrs?: Dimensions | null): void;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export interface DistinctHandle {
|
|
48
|
+
add(identifier: string): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
47
51
|
export type StopTimer = (dims?: Dimensions | null) => void;
|
|
48
52
|
|
|
49
53
|
export interface SdkDefaults {
|
|
@@ -140,6 +144,12 @@ export type LogRow = [timestamp: number, level: LogLevel, message: string, attrs
|
|
|
140
144
|
export type CounterRow = [name: string, dims: Dimensions | null, value: number];
|
|
141
145
|
export type GaugeRow = [name: string, dims: Dimensions | null, value: number, timestamp: number];
|
|
142
146
|
export type HistogramRow = [name: string, dims: Dimensions | null, snapshot: HistogramSnapshot];
|
|
147
|
+
export type DistinctRow = [name: string, dims: Dimensions | null, sketch: HllSketch];
|
|
148
|
+
|
|
149
|
+
export interface HllSketch {
|
|
150
|
+
precision: 9;
|
|
151
|
+
registers: string;
|
|
152
|
+
}
|
|
143
153
|
|
|
144
154
|
export interface HistogramExemplar {
|
|
145
155
|
value: number;
|
|
@@ -159,6 +169,7 @@ export interface MetricsSnapshot {
|
|
|
159
169
|
counters: CounterRow[];
|
|
160
170
|
gauges: GaugeRow[];
|
|
161
171
|
histograms: HistogramRow[];
|
|
172
|
+
distincts?: DistinctRow[];
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
export interface Frame {
|
|
@@ -177,6 +188,7 @@ export interface FrameBatch {
|
|
|
177
188
|
droppedCounters: number;
|
|
178
189
|
droppedGauges: number;
|
|
179
190
|
droppedHistograms: number;
|
|
191
|
+
droppedDistincts: number;
|
|
180
192
|
droppedLogs: number;
|
|
181
193
|
droppedEvents: number;
|
|
182
194
|
}
|
|
@@ -197,7 +209,7 @@ export interface InternalSnapshot {
|
|
|
197
209
|
}
|
|
198
210
|
|
|
199
211
|
export interface MeasureTraceRecord {
|
|
200
|
-
type: 'counter' | 'gauge' | 'histogram';
|
|
212
|
+
type: 'counter' | 'gauge' | 'histogram' | 'distinct';
|
|
201
213
|
name: string;
|
|
202
214
|
dims: Dimensions | null;
|
|
203
215
|
op: 'inc' | 'add' | 'set' | 'observe';
|
|
@@ -226,6 +238,7 @@ export interface FrameTraceRecord {
|
|
|
226
238
|
counters: number;
|
|
227
239
|
gauges: number;
|
|
228
240
|
histograms: number;
|
|
241
|
+
distincts: number;
|
|
229
242
|
events: number;
|
|
230
243
|
logs: number;
|
|
231
244
|
droppedLogs: number;
|
|
@@ -307,11 +320,31 @@ export class Histogram implements HistogramHandle {
|
|
|
307
320
|
reset(): void;
|
|
308
321
|
}
|
|
309
322
|
|
|
323
|
+
export class HyperLogLog implements DistinctHandle {
|
|
324
|
+
name: string;
|
|
325
|
+
dims: Dimensions | null;
|
|
326
|
+
privacySalt: string;
|
|
327
|
+
registers: Uint8Array;
|
|
328
|
+
dirty: boolean;
|
|
329
|
+
constructor(name: string, dims: Dimensions | null, privacySalt: string);
|
|
330
|
+
add(identifier: string): void;
|
|
331
|
+
snapshot(): HllSketch;
|
|
332
|
+
reset(): void;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export const HLL_PRECISION: 9;
|
|
336
|
+
export function encodeHllRegisters(registers: Uint8Array): string;
|
|
337
|
+
export function decodeHllRegisters(sketch: HllSketch): Uint8Array;
|
|
338
|
+
export function estimateHllRegisters(registers: Uint8Array): number;
|
|
339
|
+
export function estimateHyperLogLog(sketch: HllSketch): number;
|
|
340
|
+
export function mergeHyperLogLog(left: HllSketch, right: HllSketch): HllSketch;
|
|
341
|
+
|
|
310
342
|
export interface MetricsRegistryOptions {
|
|
311
343
|
maxSeriesPerMetric: number;
|
|
312
344
|
maxDimensionKeys: number;
|
|
313
345
|
maxDimensionValueLength: number;
|
|
314
346
|
defaultHistogramBuckets: number[];
|
|
347
|
+
privacySalt?: string;
|
|
315
348
|
onCardinalityDropped: () => void;
|
|
316
349
|
}
|
|
317
350
|
|
|
@@ -320,6 +353,7 @@ export class MetricsRegistry {
|
|
|
320
353
|
counter(name: string, dims?: Dimensions | null): CounterHandle;
|
|
321
354
|
gauge(name: string, dims?: Dimensions | null): GaugeHandle;
|
|
322
355
|
histogram(name: string, a?: HistogramOptions | null, b?: HistogramOptions | null): HistogramHandle;
|
|
356
|
+
distinct(name: string, dims?: Dimensions | null): DistinctHandle;
|
|
323
357
|
timer(name: string, dims?: Dimensions | null): StopTimer;
|
|
324
358
|
snapshotAndReset(): MetricsSnapshot;
|
|
325
359
|
isDirty(): boolean;
|
|
@@ -403,8 +437,10 @@ export class WardxCore {
|
|
|
403
437
|
counter(name: string, dims?: Dimensions | null): CounterHandle;
|
|
404
438
|
gauge(name: string, dims?: Dimensions | null): GaugeHandle;
|
|
405
439
|
histogram(name: string, a?: HistogramOptions | null, b?: HistogramOptions | null): HistogramHandle;
|
|
440
|
+
distinct(name: string, dims?: Dimensions | null): DistinctHandle;
|
|
406
441
|
timer(name: string, dims?: Dimensions | null): StopTimer;
|
|
407
442
|
event(name: string, attrs?: Attrs | null): void;
|
|
443
|
+
retentionActivity(userId: string): void;
|
|
408
444
|
identify(subjectId: string | null | undefined): void;
|
|
409
445
|
configGet<T>(key: string, fallback: T, context?: SubjectContext): T;
|
|
410
446
|
experimentGoal(name: string, context?: ExperimentGoalContext): void;
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,15 @@ export { ConfigStore } from './config/ConfigStore.js';
|
|
|
5
5
|
export { Counter } from './metrics/Counter.js';
|
|
6
6
|
export { Gauge } from './metrics/Gauge.js';
|
|
7
7
|
export { Histogram } from './metrics/Histogram.js';
|
|
8
|
+
export {
|
|
9
|
+
HLL_PRECISION,
|
|
10
|
+
HyperLogLog,
|
|
11
|
+
decodeHllRegisters,
|
|
12
|
+
encodeHllRegisters,
|
|
13
|
+
estimateHllRegisters,
|
|
14
|
+
estimateHyperLogLog,
|
|
15
|
+
mergeHyperLogLog
|
|
16
|
+
} from './metrics/HyperLogLog.js';
|
|
8
17
|
export { MetricsRegistry } from './metrics/MetricsRegistry.js';
|
|
9
18
|
export { EventBuffer } from './buffers/EventBuffer.js';
|
|
10
19
|
export { LogBuffer } from './buffers/LogBuffer.js';
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const HLL_PRECISION = 9;
|
|
4
|
+
export const HLL_REGISTER_COUNT = 1 << HLL_PRECISION;
|
|
5
|
+
export const HLL_MAX_RANK = 64 - HLL_PRECISION + 1;
|
|
6
|
+
|
|
7
|
+
function assertRegisters(registers) {
|
|
8
|
+
if (!(registers instanceof Uint8Array) || registers.length !== HLL_REGISTER_COUNT) {
|
|
9
|
+
throw new Error(`HLL registers must contain exactly ${HLL_REGISTER_COUNT} bytes`);
|
|
10
|
+
}
|
|
11
|
+
for (const rank of registers) {
|
|
12
|
+
if (rank > HLL_MAX_RANK) throw new Error(`HLL register rank must be <= ${HLL_MAX_RANK}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function rankAfterIndex(digest) {
|
|
17
|
+
let rank = 1;
|
|
18
|
+
for (let bit = HLL_PRECISION; bit < 64; bit++) {
|
|
19
|
+
if ((digest[bit >> 3] & (1 << (7 - (bit & 7)))) !== 0) return rank;
|
|
20
|
+
rank += 1;
|
|
21
|
+
}
|
|
22
|
+
return rank;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function encodeHllRegisters(registers) {
|
|
26
|
+
assertRegisters(registers);
|
|
27
|
+
return Buffer.from(registers).toString('base64');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function decodeHllRegisters(body) {
|
|
31
|
+
if (!body || body.precision !== HLL_PRECISION || typeof body.registers !== 'string') {
|
|
32
|
+
throw new Error(`HLL sketch must use precision ${HLL_PRECISION}`);
|
|
33
|
+
}
|
|
34
|
+
const decoded = Buffer.from(body.registers, 'base64');
|
|
35
|
+
if (decoded.toString('base64') !== body.registers) throw new Error('HLL registers must be canonical base64');
|
|
36
|
+
const registers = Uint8Array.from(decoded);
|
|
37
|
+
assertRegisters(registers);
|
|
38
|
+
return registers;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function estimateHllRegisters(registers) {
|
|
42
|
+
assertRegisters(registers);
|
|
43
|
+
let harmonic = 0;
|
|
44
|
+
let zeros = 0;
|
|
45
|
+
for (const rank of registers) {
|
|
46
|
+
harmonic += 2 ** -rank;
|
|
47
|
+
if (rank === 0) zeros += 1;
|
|
48
|
+
}
|
|
49
|
+
const m = HLL_REGISTER_COUNT;
|
|
50
|
+
const alpha = 0.7213 / (1 + 1.079 / m);
|
|
51
|
+
const raw = alpha * m * m / harmonic;
|
|
52
|
+
const corrected = raw <= 2.5 * m && zeros > 0 ? m * Math.log(m / zeros) : raw;
|
|
53
|
+
return Math.round(corrected);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function estimateHyperLogLog(body) {
|
|
57
|
+
return estimateHllRegisters(decodeHllRegisters(body));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function mergeHyperLogLog(left, right) {
|
|
61
|
+
const leftRegisters = decodeHllRegisters(left);
|
|
62
|
+
const rightRegisters = decodeHllRegisters(right);
|
|
63
|
+
for (let index = 0; index < leftRegisters.length; index++) {
|
|
64
|
+
if (rightRegisters[index] > leftRegisters[index]) leftRegisters[index] = rightRegisters[index];
|
|
65
|
+
}
|
|
66
|
+
return { precision: HLL_PRECISION, registers: encodeHllRegisters(leftRegisters) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class HyperLogLog {
|
|
70
|
+
constructor(name, dims, privacySalt) {
|
|
71
|
+
if (typeof privacySalt !== 'string' || privacySalt.length === 0) {
|
|
72
|
+
throw new Error('distinct requires a non-empty privacySalt');
|
|
73
|
+
}
|
|
74
|
+
this.name = name;
|
|
75
|
+
this.dims = dims;
|
|
76
|
+
this.privacySalt = privacySalt;
|
|
77
|
+
this.registers = new Uint8Array(HLL_REGISTER_COUNT);
|
|
78
|
+
this.dirty = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
add(identifier) {
|
|
82
|
+
if (typeof identifier !== 'string' || identifier.length === 0) {
|
|
83
|
+
throw new Error('distinct.add requires a non-empty string');
|
|
84
|
+
}
|
|
85
|
+
const digest = createHash('sha256')
|
|
86
|
+
.update(this.privacySalt, 'utf8')
|
|
87
|
+
.update('\0', 'utf8')
|
|
88
|
+
.update(identifier, 'utf8')
|
|
89
|
+
.digest();
|
|
90
|
+
const index = (digest[0] << 1) | (digest[1] >> 7);
|
|
91
|
+
const rank = rankAfterIndex(digest);
|
|
92
|
+
if (rank > this.registers[index]) this.registers[index] = rank;
|
|
93
|
+
this.dirty = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
snapshot() {
|
|
97
|
+
return { precision: HLL_PRECISION, registers: encodeHllRegisters(this.registers) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
reset() {
|
|
101
|
+
this.registers.fill(0);
|
|
102
|
+
this.dirty = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const NOOP_DISTINCT = Object.freeze({ add() {} });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Counter, NOOP_COUNTER } from './Counter.js';
|
|
2
2
|
import { Gauge, NOOP_GAUGE } from './Gauge.js';
|
|
3
3
|
import { Histogram, NOOP_HISTOGRAM } from './Histogram.js';
|
|
4
|
+
import { HyperLogLog, NOOP_DISTINCT } from './HyperLogLog.js';
|
|
4
5
|
import { startTimer } from './Timer.js';
|
|
5
6
|
import { assertMetricName, dimKey, validateDimensions } from './dimensions.js';
|
|
6
7
|
|
|
@@ -48,16 +49,24 @@ export class MetricsRegistry {
|
|
|
48
49
|
this.maxDimensionKeys = options.maxDimensionKeys;
|
|
49
50
|
this.maxDimensionValueLength = options.maxDimensionValueLength;
|
|
50
51
|
this.defaultHistogramBuckets = options.defaultHistogramBuckets;
|
|
52
|
+
this.privacySalt = options.privacySalt;
|
|
51
53
|
this.onCardinalityDropped = options.onCardinalityDropped;
|
|
52
54
|
this.countersByName = new Map();
|
|
53
55
|
this.gaugesByName = new Map();
|
|
54
56
|
this.histogramsByName = new Map();
|
|
57
|
+
this.distinctsByName = new Map();
|
|
55
58
|
this.rejected = new Set();
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
_series(kindMap, Ctor, name, dims, extra) {
|
|
59
62
|
assertMetricName(name);
|
|
60
|
-
const kindPrefix = kindMap === this.histogramsByName
|
|
63
|
+
const kindPrefix = kindMap === this.histogramsByName
|
|
64
|
+
? 'h'
|
|
65
|
+
: kindMap === this.gaugesByName
|
|
66
|
+
? 'g'
|
|
67
|
+
: kindMap === this.distinctsByName
|
|
68
|
+
? 'd'
|
|
69
|
+
: 'c';
|
|
61
70
|
const checked = validateDimensions(dims, this.maxDimensionKeys, this.maxDimensionValueLength);
|
|
62
71
|
const key = dimKey(checked.ok ? checked.dims : dims);
|
|
63
72
|
const rejectKey = kindPrefix + '\0' + name + '\0' + key;
|
|
@@ -109,6 +118,13 @@ export class MetricsRegistry {
|
|
|
109
118
|
return series;
|
|
110
119
|
}
|
|
111
120
|
|
|
121
|
+
distinct(name, dims) {
|
|
122
|
+
const series = this._series(this.distinctsByName, HyperLogLog, name, dims, (resolvedDims) => {
|
|
123
|
+
return new HyperLogLog(name, resolvedDims, this.privacySalt);
|
|
124
|
+
});
|
|
125
|
+
return series || NOOP_DISTINCT;
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
timer(name, dims) {
|
|
113
129
|
const histogram = this.histogram(name, dims);
|
|
114
130
|
return startTimer((duration, endDims) => {
|
|
@@ -148,7 +164,16 @@ export class MetricsRegistry {
|
|
|
148
164
|
}
|
|
149
165
|
}
|
|
150
166
|
}
|
|
151
|
-
|
|
167
|
+
const distincts = [];
|
|
168
|
+
for (const byKey of this.distinctsByName.values()) {
|
|
169
|
+
for (const series of byKey.values()) {
|
|
170
|
+
if (series.dirty) {
|
|
171
|
+
distincts.push([series.name, series.dims, series.snapshot()]);
|
|
172
|
+
series.reset();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { counters, gauges, histograms, distincts };
|
|
152
177
|
}
|
|
153
178
|
|
|
154
179
|
isDirty() {
|
|
@@ -167,6 +192,11 @@ export class MetricsRegistry {
|
|
|
167
192
|
if (series.count > 0) return true;
|
|
168
193
|
}
|
|
169
194
|
}
|
|
195
|
+
for (const byKey of this.distinctsByName.values()) {
|
|
196
|
+
for (const series of byKey.values()) {
|
|
197
|
+
if (series.dirty) return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
170
200
|
return false;
|
|
171
201
|
}
|
|
172
202
|
}
|
package/src/trace/wrap.js
CHANGED
|
@@ -44,3 +44,14 @@ export function wrapHistogram(tracer, series, noop, name, dims) {
|
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
export function wrapDistinct(tracer, series, noop, name, dims) {
|
|
49
|
+
const n = noop ? name : series.name;
|
|
50
|
+
const d = noop ? (dims ?? null) : series.dims;
|
|
51
|
+
return {
|
|
52
|
+
add(identifier) {
|
|
53
|
+
series.add(identifier);
|
|
54
|
+
emit(tracer, 'measure', { type: 'distinct', name: n, dims: d, op: 'add', value: 1, noop });
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|