-
-
Notifications
You must be signed in to change notification settings - Fork 318
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
670 additions
and
0 deletions.
There are no files selected for viewing
38 changes: 38 additions & 0 deletions
38
packages/beacon-node/test/unit/monitoring/clientStats.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import {expect} from "chai"; | ||
import {ClientStats} from "../../../src/monitoring/types.js"; | ||
import {createClientStats} from "../../../src/monitoring/clientStats.js"; | ||
import {beaconNodeStatsSchema, ClientStatsSchema, systemStatsSchema, validatorStatsSchema} from "./schemas.js"; | ||
|
||
describe("monitoring / clientStats", () => { | ||
describe("BeaconNodeStats", () => { | ||
it("should contain all required keys", () => { | ||
const beaconNodeStats = createClientStats("beacon")[0]; | ||
|
||
expect(getJsonKeys(beaconNodeStats)).to.have.all.members(getSchemaKeys(beaconNodeStatsSchema)); | ||
}); | ||
}); | ||
|
||
describe("ValidatorStats", () => { | ||
it("should contain all required keys", () => { | ||
const validatorNodeStats = createClientStats("validator")[0]; | ||
|
||
expect(getJsonKeys(validatorNodeStats)).to.have.all.members(getSchemaKeys(validatorStatsSchema)); | ||
}); | ||
}); | ||
|
||
describe("SystemStats", () => { | ||
it("should contain all required keys", () => { | ||
const systemStats = createClientStats("beacon", true)[1]; | ||
|
||
expect(getJsonKeys(systemStats)).to.have.all.members(getSchemaKeys(systemStatsSchema)); | ||
}); | ||
}); | ||
}); | ||
|
||
function getJsonKeys(stats: ClientStats): string[] { | ||
return Object.values(stats).map((property) => property.definition.jsonKey); | ||
} | ||
|
||
function getSchemaKeys(schema: ClientStatsSchema): string[] { | ||
return schema.map((s) => s.key); | ||
} |
247 changes: 247 additions & 0 deletions
247
packages/beacon-node/test/unit/monitoring/properties.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
import {expect} from "chai"; | ||
import {IMetrics} from "../../../src/metrics/index.js"; | ||
import {DynamicProperty, MetricProperty, StaticProperty} from "../../../src/monitoring/properties.js"; | ||
import {JsonType} from "../../../src/monitoring/types.js"; | ||
import {createMetricsTest} from "../metrics/utils.js"; | ||
|
||
describe("monitoring / properties", () => { | ||
const jsonKey = "test_key"; | ||
const value = 1; | ||
|
||
describe("StaticProperty", () => { | ||
it("should return a json record with the configured key and value", () => { | ||
const staticProperty = new StaticProperty({jsonKey, value}); | ||
|
||
const jsonRecord = staticProperty.getRecord(); | ||
|
||
expect(jsonRecord.key).to.equal(jsonKey); | ||
expect(jsonRecord.value).to.equal(value); | ||
}); | ||
}); | ||
|
||
describe("DynamicProperty", () => { | ||
it("should return a json record with the configured key and return value of provider", async () => { | ||
const dynamicProperty = new DynamicProperty({jsonKey, provider: () => value}); | ||
|
||
const jsonRecord = await dynamicProperty.getRecord(); | ||
|
||
expect(jsonRecord.key).to.equal(jsonKey); | ||
expect(jsonRecord.value).to.equal(value); | ||
}); | ||
|
||
it("should return the same value on consecutive calls if cacheResult is set to true", async () => { | ||
const initialValue = 1; | ||
let updatedValue = initialValue; | ||
|
||
const provider = (): number => { | ||
const value = updatedValue; | ||
updatedValue++; | ||
return value; | ||
}; | ||
|
||
const dynamicProperty = new DynamicProperty({jsonKey, provider, cacheResult: true}); | ||
|
||
// ensure consecutive calls still return initial provider value | ||
expect((await dynamicProperty.getRecord()).value).to.equal(initialValue); | ||
expect((await dynamicProperty.getRecord()).value).to.equal(initialValue); | ||
expect((await dynamicProperty.getRecord()).value).to.equal(initialValue); | ||
}); | ||
}); | ||
|
||
describe("MetricProperty", () => { | ||
let metrics: IMetrics; | ||
|
||
before(() => { | ||
metrics = createMetricsTest(); | ||
}); | ||
|
||
it("should return a json record with the configured key and metric value", async () => { | ||
const peerCount = 50; | ||
metrics.peers.set(peerCount); | ||
|
||
const metricProperty = new MetricProperty({ | ||
jsonKey, | ||
metricName: "libp2p_peers", | ||
jsonType: JsonType.Number, | ||
defaultValue: 0, | ||
}); | ||
|
||
const jsonRecord = await metricProperty.getRecord(metrics.register); | ||
|
||
expect(jsonRecord.key).to.equal(jsonKey); | ||
expect(jsonRecord.value).to.equal(peerCount); | ||
}); | ||
|
||
it("should return the default value if metric with name does not exist", async () => { | ||
const defaultValue = 10; | ||
|
||
const metricProperty = new MetricProperty({ | ||
jsonKey, | ||
metricName: "does_not_exist", | ||
jsonType: JsonType.Number, | ||
defaultValue, | ||
}); | ||
|
||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(defaultValue); | ||
}); | ||
|
||
it("should get the value from label instead of metric value if fromLabel is defined", async () => { | ||
const metricName = "static_metric"; | ||
const labelName = "test_label"; | ||
const labelValue = "test_value"; | ||
|
||
metrics.register.static({name: metricName, help: "fromLabel test", value: {[labelName]: labelValue}}); | ||
|
||
const metricProperty = new MetricProperty({ | ||
jsonKey, | ||
metricName, | ||
fromLabel: labelName, | ||
jsonType: JsonType.String, | ||
defaultValue: "", | ||
}); | ||
|
||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(labelValue); | ||
}); | ||
|
||
it("should get the value from metric with label if withLabel is defined", async () => { | ||
const metricName = "metric_with_labels"; | ||
const labelName = "test_label_name"; | ||
const labelValue = "test_label_value"; | ||
const metricValue = 10; | ||
|
||
const metric = metrics.register.gauge({name: metricName, help: "withLabel test", labelNames: [labelName]}); | ||
metric.set({[labelName]: "different_value"}, metricValue + 1); | ||
metric.set({[labelName]: labelValue}, metricValue); | ||
|
||
const metricProperty = new MetricProperty({ | ||
jsonKey, | ||
metricName, | ||
withLabel: {name: labelName, value: labelValue}, | ||
jsonType: JsonType.Number, | ||
defaultValue: 0, | ||
}); | ||
|
||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(metricValue); | ||
}); | ||
|
||
it("should return the same value on consecutive calls if cacheResult is set to true", async () => { | ||
const metricName = "metric_test_caching"; | ||
const initialValue = 10; | ||
|
||
const metric = metrics.register.gauge({name: metricName, help: "cacheResult test"}); | ||
metric.set(initialValue); | ||
|
||
const metricProperty = new MetricProperty({ | ||
jsonKey, | ||
metricName, | ||
jsonType: JsonType.Number, | ||
defaultValue: 0, | ||
cacheResult: true, | ||
}); | ||
|
||
// initial call which will cache the result | ||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(initialValue); | ||
|
||
// set different value | ||
metric.set(initialValue + 1); | ||
|
||
// ensure consecutive calls still return initial value | ||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(initialValue); | ||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(initialValue); | ||
}); | ||
|
||
it("should convert the metric value to a string if jsonType is JsonType.String", async () => { | ||
const metricName = "metric_test_string"; | ||
|
||
const metric = metrics.register.gauge({name: metricName, help: "JsonType.String test"}); | ||
|
||
const metricProperty = new MetricProperty({ | ||
jsonKey, | ||
metricName, | ||
jsonType: JsonType.String, | ||
defaultValue: "", | ||
}); | ||
|
||
metric.set(10); | ||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal("10"); | ||
}); | ||
|
||
it("should round the metric value to the nearest integer if jsonType is JsonType.Number", async () => { | ||
const metricName = "metric_test_number"; | ||
|
||
const metric = metrics.register.gauge({name: metricName, help: "JsonType.Number test"}); | ||
|
||
const metricProperty = new MetricProperty({ | ||
jsonKey, | ||
metricName, | ||
jsonType: JsonType.Number, | ||
defaultValue: 0, | ||
}); | ||
|
||
metric.set(1.49); | ||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(1); | ||
}); | ||
|
||
it("should convert the metric value to a boolean if jsonType is JsonType.Boolean", async () => { | ||
const metricName = "metric_test_boolean"; | ||
|
||
const metric = metrics.register.gauge({name: metricName, help: "JsonType.Boolean test"}); | ||
|
||
const metricProperty = new MetricProperty({ | ||
jsonKey, | ||
metricName, | ||
jsonType: JsonType.Boolean, | ||
defaultValue: false, | ||
}); | ||
|
||
metric.set(0); | ||
// metric value of 0 should be converted to false | ||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(false); | ||
|
||
metric.set(1); | ||
// metric value > 0 should be converted to true | ||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(true); | ||
}); | ||
|
||
it("should convert the metric value to true if the specified rangeValue is matched", async () => { | ||
const metricName = "metric_test_range_value"; | ||
const rangeValue = 3; | ||
|
||
const metric = metrics.register.gauge({name: metricName, help: "rangeValue test"}); | ||
|
||
const metricProperty = new MetricProperty({ | ||
jsonKey, | ||
metricName, | ||
rangeValue, | ||
jsonType: JsonType.Boolean, | ||
defaultValue: false, | ||
}); | ||
|
||
metric.set(rangeValue + 1); | ||
// value does not match range value and should be converted to false | ||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(false); | ||
|
||
metric.set(rangeValue); | ||
// value matches range value and should be converted to true | ||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(true); | ||
}); | ||
|
||
it("should apply the defined formatter to the metric value", async () => { | ||
const metricName = "metric_test_formatting"; | ||
const metricValue = 10; | ||
|
||
const metric = metrics.register.gauge({name: metricName, help: "formatter test"}); | ||
|
||
const metricProperty = new MetricProperty({ | ||
jsonKey, | ||
metricName, | ||
jsonType: JsonType.String, | ||
formatter: (value) => `prefix_${value}`, | ||
defaultValue: "", | ||
}); | ||
|
||
metric.set(metricValue); | ||
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(`prefix_${metricValue}`); | ||
}); | ||
}); | ||
}); |
86 changes: 86 additions & 0 deletions
86
packages/beacon-node/test/unit/monitoring/remoteService.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import {expect} from "chai"; | ||
import fastify from "fastify"; | ||
import {RemoteServiceError} from "../../../src/monitoring/service.js"; | ||
import {ProcessType} from "../../../src/monitoring/types.js"; | ||
import {beaconNodeStatsSchema, ClientStatsSchema, systemStatsSchema, validatorStatsSchema} from "./schemas.js"; | ||
|
||
/* eslint-disable no-console */ | ||
|
||
type ReceivedData = Record<string, unknown>; | ||
|
||
export const remoteServiceRoutes = { | ||
success: "/success", | ||
error: "/error", | ||
pending: "/pending", | ||
}; | ||
|
||
export const remoteServiceError: RemoteServiceError = {status: "error", data: null}; | ||
|
||
/** | ||
* Starts mocked remote service to receive and validate client stats | ||
*/ | ||
export async function startRemoteService(): Promise<{baseUrl: URL}> { | ||
const server = fastify(); | ||
|
||
server.post(remoteServiceRoutes.success, {}, async function (request, reply) { | ||
if (Array.isArray(request.body)) { | ||
request.body.forEach(validateRequestData); | ||
} else { | ||
validateRequestData(request.body as ReceivedData); | ||
} | ||
|
||
return reply.status(200).send(); | ||
}); | ||
|
||
server.post(remoteServiceRoutes.error, {}, async function (_request, reply) { | ||
return reply.status(400).send(remoteServiceError); | ||
}); | ||
|
||
server.post(remoteServiceRoutes.pending, {}, function () { | ||
// keep request pending until timeout is reached or aborted | ||
}); | ||
|
||
server.addHook("onError", (_request, _reply, error, done) => { | ||
console.log(`Error: ${error.message}`); | ||
done(); | ||
}); | ||
|
||
// ask the operating system to assign a free (ephemeral) port | ||
// and use IPv4 localhost "127.0.0.1" to avoid known IPv6 issues | ||
const baseUrl = await server.listen({host: "127.0.0.1", port: 0}); | ||
|
||
after(() => { | ||
// there is no need to wait for server to be closed | ||
server.close().catch(console.log); | ||
}); | ||
|
||
return {baseUrl: new URL(baseUrl)}; | ||
} | ||
|
||
function validateRequestData(data: ReceivedData): void { | ||
switch (data.process) { | ||
case ProcessType.BeaconNode: | ||
validateClientStats(data, beaconNodeStatsSchema); | ||
break; | ||
case ProcessType.Validator: | ||
validateClientStats(data, validatorStatsSchema); | ||
break; | ||
case ProcessType.System: | ||
validateClientStats(data, systemStatsSchema); | ||
break; | ||
default: | ||
throw new Error(`Invalid process type "${data.process}"`); | ||
} | ||
} | ||
|
||
function validateClientStats(data: ReceivedData, schema: ClientStatsSchema): void { | ||
schema.forEach((s) => { | ||
try { | ||
expect(data[s.key]).to.be.a(s.type); | ||
} catch { | ||
throw new Error( | ||
`Validation of property "${s.key}" failed. Expected type "${s.type}" but received "${typeof data[s.key]}".` | ||
); | ||
} | ||
}); | ||
} |
Oops, something went wrong.