Skip to content

Commit

Permalink
test(cosmic-swingset): Add test coverage for Prometheus output
Browse files Browse the repository at this point in the history
Fixes #10911
  • Loading branch information
gibson042 committed Feb 6, 2025
1 parent 936de39 commit 1572695
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 0 deletions.
71 changes: 71 additions & 0 deletions packages/cosmic-swingset/test/prometheus.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import test from 'ava';

import fs from 'node:fs';
import path from 'node:path';

import { q, Fail } from '@endo/errors';
import tmp from 'tmp';

import { promisify } from '@agoric/internal/src/js-utils.js';
import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js';

import { makeLaunchChain } from '../src/chain-main.js';
import { defaultBootstrapMessage } from '../tools/test-kit.js';

const tmpDir = promisify(tmp.dir);

test('Prometheus metric definitions', async t => {
const OTEL_EXPORTER_PROMETHEUS_PORT = '12345';
// @ts-expect-error
const { storagePort } = defaultBootstrapMessage;
const [dbDir, cleanupDB] = await tmpDir({
prefix: 'testdb',
unsafeCleanup: true,
});
t.teardown(cleanupDB);

const fakeStorageKit = makeFakeStorageKit('');
const { toStorage: handleVstorage } = fakeStorageKit;
const fakeAgcc = {
send: (destPort, msgJson) => {
const msg = JSON.parse(msgJson);
if (destPort === storagePort) return JSON.stringify(handleVstorage(msg));
throw Fail`port ${q(destPort)} not implemented for message ${msg}`;
},
};
const launchChain = makeLaunchChain(fakeAgcc, dbDir, {
env: {
OTEL_EXPORTER_PROMETHEUS_PORT,
CHAIN_BOOTSTRAP_VAT_CONFIG:
'@agoric/vm-config/decentral-core-config.json',
},
fs,
path,
});

const { shutdown } = await launchChain(defaultBootstrapMessage);
t.teardown(shutdown);

const response = await fetch(
`http://localhost:${OTEL_EXPORTER_PROMETHEUS_PORT}/metrics`,
);
const text = await response.text();
// Normalize text:
// https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-details
// * Set telemetry_sdk_version and service_instance_id to "%s".
// * Replace integer values with "%d" and floating-point values with "%f".
// * Replace trailing milliseconds-since-epoch timestamps with "%@".
const normalizedText = text
.replace(/^.*(telemetry_sdk_version|service_instance_id).*$/m, line =>
line.replaceAll(
/(telemetry_sdk_version|service_instance_id)="([^""\\]|\\.)*"/g,
'$1="%s"',
),
)
.replaceAll(
/^([^#].*?) (?:([0-9]+)|([0-9]*[.][0-9]+))( [0-9]{13,})?$/gm,
(_substring, prefix, intValue, floatValue, timestamp) =>
`${prefix} ${intValue ? '%d' : '%f'}${timestamp ? ' %@' : ''}`,
);
t.snapshot(normalizedText);
});
84 changes: 84 additions & 0 deletions packages/internal/src/js-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,48 @@
* dependent upon a hardened environment.
*/

/**
* Pick the last type from an array.
*
* @template {unknown[]} A
* @typedef {A extends [...infer P, infer X] ? X : never} Last
*/
/**
* Collect up to 8 function overloads into a union.
*
* @template {(...args: any[]) => unknown} F
* @typedef {F extends {
* (...args: infer A1): infer R1;
* (...args: infer A2): infer R2;
* (...args: infer A3): infer R3;
* (...args: infer A4): infer R4;
* (...args: infer A5): infer R5;
* (...args: infer A6): infer R6;
* (...args: infer A7): infer R7;
* }
* ?
* | ((...args: A1) => R1)
* | ((...args: A2) => R2)
* | ((...args: A3) => R3)
* | ((...args: A4) => R4)
* | ((...args: A5) => R5)
* | ((...args: A6) => R6)
* | ((...args: A7) => R7)
* : never} Overloads
*/
/**
* Pick the type of a function's last parameter.
*
* @template {(...args: any[]) => unknown} F
* @typedef {Last<Parameters<Overloads<F>>>} LastParameter
*/

const { defineProperties } = Object;

/** @type {<F>(name: string, fn: F) => F} */
export const defineName = (name, fn) =>
defineProperties(fn, { name: { value: name } });

/**
* Deep-copy a value by round-tripping it through JSON (which drops
* function/symbol/undefined values and properties that are non-enumerable
Expand Down Expand Up @@ -87,3 +129,45 @@ export const makeMeasureSeconds = currentTimeMillisec => {
};
return measureSeconds;
};

/**
* Generalize Node.js util.promisify to support callbacks with multiple
* post-error parameters, which fulfill the returned promise as an array rather
* than as a single value.
*
* @template {(...args: any[]) => unknown} F function whose last parameter is an
* error-first callback
* @template {Parameters<LastParameter<F>> extends [Error | null, ...infer R]
* ? R
* : never} R
* @param {F} fn
*/
export const promisify = fn => {
/**
* @typedef {Parameters<Overloads<F>> extends [...infer P, infer CB]
* ? (
* ...args: P
* ) => Promise<
* Parameters<LastParameter<F>> extends [unknown, unknown] ? R[0] : R
* >
* : never} Promisified
*/
const promisified = /** @type {Promisified} */ (
(...args) =>
// eslint-disable-next-line no-restricted-syntax
new Promise((resolve, reject) => {
/** @type {(err: Error, ...result: R) => void} */
const callback = (err, ...result) => {
if (err) {
reject(err);
} else if (result.length === 1) {
resolve(/** @type {any} */ (result[0]));
} else {
resolve(result);
}
};
fn(...args, callback);
})
);
return defineName(fn.name && `promisified ${fn.name}`, promisified);
};
2 changes: 2 additions & 0 deletions packages/internal/test/snapshots/exports.test.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Generated by [AVA](https://avajs.dev).
'deepCopyJsonable',
'deepMapObject',
'deeplyFulfilledObject',
'defineName',
'forever',
'fromUniqueEntries',
'getMethodNames',
Expand All @@ -35,6 +36,7 @@ Generated by [AVA](https://avajs.dev).
'mustMatch',
'objectMap',
'objectMetaMap',
'promisify',
'pureDataMarshaller',
'synchronizedTee',
'untilTrue',
Expand Down
Binary file modified packages/internal/test/snapshots/exports.test.js.snap
Binary file not shown.

0 comments on commit 1572695

Please sign in to comment.