Skip to content

Commit

Permalink
feat: added metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
CptSchnitz committed Oct 26, 2024
1 parent 9205f2e commit 743a562
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 8 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,5 @@ dist

# reports of jest tests
reports

src/version.ts
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ This package allows you to configure various options for loading and managing co
- **Default**: `./config`
- **Description**: The path to the local configuration folder.

### `metricsRegistry`
- **Type**: `promClient.Registry`
- **Optional**: `true`
- **Description**: The prometheus registry to use for metrics. If not provided, metrics are not provided.

## Environment Variable Configuration

The following environment variables can be used to configure the options:
Expand Down Expand Up @@ -243,6 +248,11 @@ try {
```
- **Description**: This error occurs when there is a version mismatch between the remote and local schemas. The payload includes the versions of both the remote and local schemas.

### `promClientNotInstalledError`
- **Code**: `8`
- **Payload**: `Error`
- **Description**: This error occurs when the `prom-client` package is not installed. The payload contains the error object.

# Debugging
If for some reason you want to debug the package you can either use the `getConfigParts` or the `getResolvedOptions` functions described in the API or use the more powerful debug logger.

Expand Down
50 changes: 49 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 13 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
"prelint": "npm run format",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"pretest": "npm run generate:version",
"test": "cross-env SUPPRESS_NO_CONFIG_WARNING=true NODE_CONFIG_STRICT_MODE=false jest --config=./tests/configurations/jest.config.js",
"prebuild": "npm run clean",
"prebuild": "npm run clean && npm run generate:version",
"build": "tsc --project tsconfig.build.json",
"start": "npm run build && cd dist && node ./index.js",
"clean": "rimraf dist",
"prepublish": "npm run build"
"prepublish": "npm run build",
"generate:version": "node scripts/generateVersionFile.mjs"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -61,7 +63,13 @@
"undici": "^6.18.2"
},
"peerDependencies": {
"@map-colonies/schemas": "^1.0.0"
"@map-colonies/schemas": "^1.0.0",
"prom-client": "^15.0.0"
},
"peerDependenciesMeta": {
"prom-client": {
"optional": true
}
},
"devDependencies": {
"@commitlint/cli": "^11.0.0",
Expand All @@ -80,6 +88,7 @@
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
"ts-jest": "^29.1.5",
"typescript": "^5.4.5"
"typescript": "^5.4.5",
"prom-client": "^15.0.0"
}
}
10 changes: 10 additions & 0 deletions scripts/generateVersionFile.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { writeFileSync } from 'fs';
import { readPackageJsonSync } from '@map-colonies/read-pkg';

const packageJson = readPackageJsonSync();
const version = packageJson.version;
const versionFile = 'src/version.ts';

const content = `/* prettier-ignore */\n/* eslint-disable*/\nexport const PACKAGE_VERSION = '${version}';\n`;

writeFileSync(versionFile, content);
12 changes: 9 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import configPkg from 'config';
import semver from 'semver';
import lodash, { type GetFieldType } from 'lodash';
import { getEnvValues } from './env';
import { BaseOptions, ConfigOptions, ConfigInstance } from './types';
import { BaseOptions, ConfigOptions, ConfigInstance, Config } from './types';
import { loadSchema } from './schemas';
import { getOptions, initializeOptions } from './options';
import { getRemoteConfig, getServerCapabilities } from './httpClient';
import { ajvConfigValidator, validate } from './validator';
import { createDebug } from './utils/debug';
import { LOCAL_SCHEMAS_PACKAGE_VERSION } from './constants';
import { createConfigError } from './errors';
import { initializeMetrics } from './metrics';

const debug = createDebug('config');

Expand All @@ -30,11 +31,12 @@ export async function config<T extends { [typeSymbol]: unknown; $id: string }>(
): Promise<ConfigInstance<T[typeof typeSymbol]>> {
// handle package options
debug('config called with options: %j', { ...options, schema: options.schema.$id });
const { schema: baseSchema, ...unvalidatedOptions } = options;
const { schema: baseSchema, metricsRegistry, ...unvalidatedOptions } = options;
const { configName, offlineMode, version, ignoreServerIsOlderVersionError } = initializeOptions(unvalidatedOptions);

let remoteConfig: object | T = {};

let serverConfigResponse: Config | undefined = undefined;
// handle remote config
if (offlineMode !== true) {
debug('handling fetching remote data');
Expand All @@ -58,7 +60,7 @@ export async function config<T extends { [typeSymbol]: unknown; $id: string }>(
}

// get the remote config
const serverConfigResponse = await getRemoteConfig(configName, version);
serverConfigResponse = await getRemoteConfig(configName, version);

if (serverConfigResponse.schemaId !== baseSchema.$id) {
debug('schema version mismatch. local: %s, remote: %s', baseSchema.$id, serverConfigResponse.schemaId);
Expand Down Expand Up @@ -100,6 +102,10 @@ export async function config<T extends { [typeSymbol]: unknown; $id: string }>(
// freeze the merged config so it can't be modified by the package user
Object.freeze(validatedConfig);

if (metricsRegistry) {
initializeMetrics(metricsRegistry, baseSchema.$id, serverConfigResponse?.version);
}

function get<TPath extends string>(path: TPath): GetFieldType<T[typeof typeSymbol], TPath> {
debug('get called with path: %s', path);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
Expand Down
1 change: 1 addition & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const configErrors = {
schemaNotFoundError: { code: 5, payload: {} as { schemaPath: string } },
schemasPackageVersionMismatchError: { code: 6, payload: {} as { remotePackageVersion: string; localPackageVersion: string } },
schemaVersionMismatchError: { code: 7, payload: {} as { remoteSchemaVersion: string; localSchemaVersion: string } },
promClientNotInstalledError: { code: 8, payload: {} as { message: string } },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as const satisfies Record<string, { code: number; payload: any }>;

Expand Down
59 changes: 59 additions & 0 deletions src/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Registry } from 'prom-client';
import { getOptions } from './options';
import { PACKAGE_VERSION } from './version';
import { LOCAL_SCHEMAS_PACKAGE_VERSION } from './constants';
import { createDebug } from './utils/debug';
import { createConfigError } from './errors';

const debug = createDebug('metrics');
const MILLISECONDS_PER_SECOND = 1000;

let promClient: typeof import('prom-client') | undefined;

function loadPromClient(): void {
if (promClient === undefined) {
debug('loading prom-client');
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
promClient = require('prom-client');
} catch (error) {
console.log('error', error);

throw createConfigError('promClientNotInstalledError', 'prom-client is not installed and metrics was initialized', error as Error);
}
}
}

export function initializeMetrics(registry: Registry, schemaId: string, actualVersion: number | undefined): void {
debug('initializing metrics');
loadPromClient();

if (promClient === undefined) {
return;
}

const { offlineMode, configName, version } = getOptions();
const gauge = new promClient.Gauge({
name: 'config_time_of_last_fetch_unix_timestamp',
help: 'The time of the last fetch of the configuration in unix timestamp',
labelNames: ['name', 'request_version', 'actual_version', 'offline_mode', 'schemas_package_version', 'package_version', 'schema_id'],
});

/* eslint-disable @typescript-eslint/naming-convention */
gauge.set(
{
name: configName,
request_version: version,
actual_version: actualVersion,
offline_mode: String(offlineMode ?? false),
schemas_package_version: LOCAL_SCHEMAS_PACKAGE_VERSION,
package_version: PACKAGE_VERSION,
schema_id: schemaId,
},

Date.now() / MILLISECONDS_PER_SECOND
);

/* eslint-enable @typescript-eslint/naming-convention */
registry.registerMetric(gauge);
}
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { typeSymbol } from '@map-colonies/schemas/build/schemas/symbol';
import { JSONSchemaType } from 'ajv';
import type { Registry } from 'prom-client';

type Prettify<T> = {
[K in keyof T]: T[K];
Expand Down Expand Up @@ -77,6 +78,11 @@ export type ConfigOptions<T extends SchemaWithType> = Prettify<
* The schema of the configuration object.
*/
schema: T;
/**
* The registry for the metrics. If not provided, the metrics will not be registered.
* Depends on the prom-client package being installed.
*/
metricsRegistry?: Registry;
}
>;

Expand Down
40 changes: 40 additions & 0 deletions tests/metrics.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Registry } from 'prom-client';
import { getOptions } from '../src/options';
import { BaseOptions } from '../src/types';
import { initializeMetrics } from '../src/metrics';
import { PACKAGE_VERSION } from '../src/version';
import { LOCAL_SCHEMAS_PACKAGE_VERSION } from '../src/constants';

jest.mock('../src/options');

const mockedGetOptions = getOptions as jest.MockedFunction<typeof getOptions>;

mockedGetOptions.mockReturnValue({
offlineMode: false,
configName: 'avi',
version: 1,
} as BaseOptions);

describe('httpClient', () => {
beforeEach(() => {});

describe('#initializeMetrics', () => {
it('should initialize the metrics in the registry with the correct labels', async () => {
const registry = new Registry();
initializeMetrics(registry, 'schema', 1);

const metric = (await registry.getMetricsAsJSON())[0];
expect(metric).toHaveProperty('name', 'config_time_of_last_fetch_unix_timestamp');
expect(metric).toHaveProperty('values[0].labels', {
/* eslint-disable @typescript-eslint/naming-convention */
actual_version: 1,
name: 'avi',
offline_mode: 'false',
package_version: PACKAGE_VERSION,
request_version: 1,
schema_id: 'schema',
schemas_package_version: LOCAL_SCHEMAS_PACKAGE_VERSION,
});
});
});
});

0 comments on commit 743a562

Please sign in to comment.