Skip to content
This repository was archived by the owner on Dec 20, 2021. It is now read-only.

Commit 8ed199f

Browse files
Merge pull request #1 from honeycombio/first-push
Initialize dynamic-sampler
2 parents dc6d94e + 9345abf commit 8ed199f

File tree

7 files changed

+5358
-0
lines changed

7 files changed

+5358
-0
lines changed

.babelrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["env"]
3+
}

README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Dynamic Sampler
2+
3+
This is a collection of samplers that can be used to provide sample
4+
rates when sending data to services like [honeycomb](https://honeycomb.io)
5+
6+
# Usage
7+
8+
### With defaults:
9+
10+
```javascript
11+
import { PerKeyThroughput } from "dynamic-sampler";
12+
const sampler = new PerKeyThroughput();
13+
14+
const rate = sampler.getSampleRate("my key");
15+
```
16+
17+
### With options
18+
19+
```javascript
20+
import { PerKeyThroughput } from "dynamic-sampler";
21+
const sampler = new PerKeyThroughput({
22+
clearFrequencySec: 100,
23+
perKeyThroughputSec: 2
24+
});
25+
```
26+
27+
## Choosing a Sampler
28+
29+
TODO
30+
31+
# Implementing New Samplers
32+
33+
The `Sampler` class includes:
34+
35+
* timer setup
36+
* construction of initial state (`Map`s)
37+
* `getSampleRate` returns the rate for a given key
38+
39+
You can extend it to create new samplers. `updateMaps` is the only
40+
function that needs to be defined, but it is often useful to collect
41+
additional configuration from the constructor:
42+
43+
```javascript
44+
import { Sampler } from "dynamic-sampler";
45+
46+
export class PerKey extends Sampler {
47+
constructor(opts = {}) {
48+
super(opts);
49+
this.perKeyThroughputSec = opts.perKeyThroughputSec || 5;
50+
}
51+
updateMaps() {
52+
if (this.currentCounts.size == 0) {
53+
//no traffic in the last 30s. clear the result Map
54+
this.savedSampleRates.clear();
55+
return;
56+
}
57+
const actualPerKeyRate = this.perKeyThroughputSec * this.clearFrequencySec;
58+
59+
const newRates = new Map();
60+
this.currentCounts.forEach((val, key) => {
61+
newRates.set(key, Math.floor(Math.max(1, val / actualPerKeyRate)));
62+
});
63+
this.savedSampleRates = newRates;
64+
}
65+
}
66+
```
67+
68+
## Modifying getSampleRate
69+
70+
Sometimes it makes sense to check additional state in `getSampleRate`
71+
and return a different result based on that. When overriding the
72+
function call `super.getSampleRate`.
73+
74+
```javascript
75+
class MySampler extends Sampler {
76+
constructor(opts = {}) {
77+
super(opts);
78+
this.hasReceivedTraffic = false;
79+
}
80+
updateMaps() {
81+
// other logic
82+
this.hasReceivedTraffic = true;
83+
}
84+
getSampleRate(key) {
85+
const superSampleRate = super.getSampleRate(key);
86+
if (!this.hasReceivedTraffic) {
87+
return this.goalSampleRate;
88+
} else {
89+
return superSampleRate;
90+
}
91+
}
92+
}
93+
```

__mocks__/nanotimer.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Mock nanotimer so we can manually tick()
2+
export default class NanoTimer {
3+
setInterval(fn, args, timeInSeconds) {
4+
this.fn = fn;
5+
this.args = args;
6+
}
7+
// custom function that is only for testing
8+
tick() {
9+
if (this.args) {
10+
this.fn(this.args);
11+
} else {
12+
this.fn();
13+
}
14+
}
15+
}

index.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import NanoTimer from "nanotimer";
2+
const debug = require("debug")("dynamic-sampler");
3+
4+
// A Sampler handles construction, timer initialization, and getting the sample
5+
// rate.
6+
export class Sampler {
7+
constructor({ clearFrequencySec } = {}) {
8+
// TODO: runtime validate inputs; make sure they're numbers
9+
this.clearFrequencySec = clearFrequencySec || 30;
10+
this.savedSampleRates = new Map();
11+
this.currentCounts = new Map();
12+
13+
if (debug.enabled) {
14+
// if debug is enabled, add a unique id to help debug
15+
this.id = (Math.random() * 100000).toFixed();
16+
this.getSampleRateCalledTimes = 0;
17+
debug("created new perKey sampler with id", this.id);
18+
} else {
19+
debug("created new perKey sampler");
20+
}
21+
22+
// Set up timer to run updateMaps() on an interval
23+
this.timer = new NanoTimer();
24+
this.timer.setInterval(
25+
this.updateMaps.bind(this),
26+
[this.id],
27+
`${this.clearFrequencySec}s`
28+
);
29+
}
30+
getSampleRate(key) {
31+
// initialize or increment an existing counter
32+
const { currentCounts, savedSampleRates } = this;
33+
if (currentCounts.has(key)) {
34+
const value = currentCounts.get(key);
35+
currentCounts.set(key, value + 1);
36+
} else {
37+
currentCounts.set(key, 1);
38+
}
39+
if (savedSampleRates.has(key)) {
40+
return savedSampleRates.get(key);
41+
} else {
42+
return 1;
43+
}
44+
}
45+
updateMaps() {
46+
throw new Error(
47+
"Classes which extend `Sampler` must define `updateMaps()`"
48+
);
49+
}
50+
}
51+
52+
export class PerKeyThroughput extends Sampler {
53+
constructor(opts = {}) {
54+
super(opts);
55+
this.perKeyThroughputSec = opts.perKeyThroughputSec || 10;
56+
}
57+
updateMaps() {
58+
debug("PerKey.updateMaps()", this.id && this.id);
59+
if (this.currentCounts.size == 0) {
60+
// no traffic in the last clearFrequencySecs. clear the result Map
61+
this.savedSampleRates.clear();
62+
return;
63+
}
64+
const actualPerKeyRate = this.perKeyThroughputSec * this.clearFrequencySec;
65+
66+
const newRates = new Map();
67+
this.currentCounts.forEach((val, key) => {
68+
newRates.set(key, Math.floor(Math.max(1, val / actualPerKeyRate)));
69+
});
70+
this.savedSampleRates = newRates;
71+
}
72+
}
73+
74+
export class AvgSampleRate extends Sampler {
75+
constructor(opts = {}) {
76+
super(opts);
77+
this.goalSampleRate = opts.goalSampleRate || 10;
78+
this.hasReceivedTraffic = false;
79+
}
80+
updateMaps() {
81+
debug("Avg.updateMaps()", this.id && this.id);
82+
if (this.currentCounts.size == 0) {
83+
//no traffic in the last 30s. clear the result Map
84+
this.savedSampleRates.clear();
85+
return;
86+
}
87+
88+
let sumEvents = 0;
89+
let logSum = 0;
90+
this.currentCounts.forEach((val, key) => {
91+
sumEvents += val;
92+
logSum += Math.log10(val);
93+
});
94+
const goalCount = sumEvents / this.goalSampleRate;
95+
const goalRatio = goalCount / logSum;
96+
97+
const newRates = new Map();
98+
let keysRemaining = this.currentCounts.size;
99+
let extra = 0;
100+
this.currentCounts.forEach((count, key) => {
101+
let goalForKey = Math.max(1, Math.log10(count) * goalRatio);
102+
const extraForKey = extra / keysRemaining;
103+
goalForKey += extraForKey;
104+
extra -= extraForKey;
105+
keysRemaining--;
106+
if (count <= goalForKey) {
107+
newRates.set(key, 1);
108+
extra += goalForKey - count;
109+
} else {
110+
newRates.set(key, Math.ceil(count / goalForKey));
111+
extra += goalForKey - count / newRates.get(key);
112+
}
113+
});
114+
this.savedSampleRates = newRates;
115+
this.hasReceivedTraffic = true;
116+
}
117+
getSampleRate(key) {
118+
debug("hasReceivedTraffic", this.hasReceivedTraffic);
119+
if (!this.hasReceivedTraffic) {
120+
return this.goalSampleRate;
121+
} else {
122+
// TODO: how well supported is this syntax
123+
// (basically no IE native support. what about babel/etc?)
124+
return super.getSampleRate(key);
125+
}
126+
}
127+
}

index.test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
jest.mock("nanotimer");
2+
import { Sampler, PerKeyThroughput, AvgSampleRate } from ".";
3+
4+
describe("Sampler", () => {
5+
test("initializes with default values", () => {
6+
const sampler = new Sampler();
7+
expect(sampler.clearFrequencySec).toEqual(30);
8+
expect(sampler.id).toBeUndefined();
9+
expect(sampler.getSampleRateCalledTimes).toBeUndefined();
10+
});
11+
});
12+
describe("PerKeyThroughput", () => {
13+
test("initializes with default values", () => {
14+
const sampler = new PerKeyThroughput();
15+
expect(sampler.clearFrequencySec).toEqual(30);
16+
expect(sampler.perKeyThroughputSec).toEqual(10);
17+
expect(sampler.id).toBeUndefined();
18+
expect(sampler.getSampleRateCalledTimes).toBeUndefined();
19+
});
20+
21+
test("gets a sample rate", () => {
22+
const sampler = new PerKeyThroughput();
23+
// Fake a bunch of traffic for a specific key
24+
new Array(1500).fill(1).forEach(() => sampler.getSampleRate("my-key"));
25+
// Mocked tick() is equal to clearFrequencySec
26+
// Moves time forward by enough to run `updateMaps()`
27+
sampler.timer.tick();
28+
// get the resulting sample rate after updating maps
29+
const a = sampler.getSampleRate("my-key");
30+
expect(a).toEqual(5);
31+
});
32+
});
33+
34+
describe("AvgSampleRate", () => {
35+
test("initializes with default values", () => {
36+
const sampler = new AvgSampleRate();
37+
console.log(sampler);
38+
expect(sampler.clearFrequencySec).toEqual(30);
39+
expect(sampler.goalSampleRate).toEqual(10);
40+
expect(sampler.id).toBeUndefined();
41+
expect(sampler.getSampleRateCalledTimes).toBeUndefined();
42+
});
43+
44+
test("gets a sample rate", () => {
45+
const sampler = new AvgSampleRate();
46+
expect(sampler.hasReceivedTraffic).toEqual(false);
47+
new Array(1500).fill(1).forEach(() => sampler.getSampleRate("my-key"));
48+
// manual tick is equal to ""
49+
sampler.timer.tick();
50+
expect(sampler.hasReceivedTraffic).toEqual(true);
51+
const a = sampler.getSampleRate("my-key");
52+
// No traffic means this is the goal sample rate
53+
expect(a).toEqual(10);
54+
sampler.timer.tick();
55+
expect(sampler.getSampleRate("my-key")).toEqual(1);
56+
});
57+
});

package.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "dynsampler",
3+
"version": "1.0.0",
4+
"description": "Dynamic sampling of events",
5+
"main": "lib/dynamic-sampler.js",
6+
"module": "lib/dynamic-sampler.m.js",
7+
"repository": "https://github.com/honeycombio/dynamic-sampler.js",
8+
"author":
9+
"Christopher Biscardi <[email protected]> (@chrisbiscardi)",
10+
"license": "Apache 2.0",
11+
"scripts": {
12+
"build": "microbundle --output lib --external all",
13+
"start": "microbundle watch --output lib --external all",
14+
"test": "jest",
15+
"precommit": "lint-staged"
16+
},
17+
"lint-staged": {
18+
"*.{js,jsx}": ["prettier --parser flow --write", "git add"],
19+
"*.json": ["prettier --parser json --write", "git add"],
20+
"*.{graphql,gql}": ["prettier --parser graphql --write", "git add"],
21+
"*.{md,markdown}": ["prettier --parser markdown --write", "git add"],
22+
"*.{css,scss}": ["prettier --parser css --write", "git add"]
23+
},
24+
"devDependencies": {
25+
"babel-core": "^6.26.0",
26+
"babel-jest": "^22.2.2",
27+
"babel-preset-env": "^1.6.1",
28+
"husky": "^0.14.3",
29+
"jest": "^22.3.0",
30+
"lint-staged": "^7.0.0",
31+
"microbundle": "^0.4.3",
32+
"prettier": "^1.10.2"
33+
},
34+
"dependencies": {
35+
"debug": "^3.1.0",
36+
"nanotimer": "^0.3.15"
37+
}
38+
}

0 commit comments

Comments
 (0)