diff --git a/Refactor-migration-guide.md b/Refactor-migration-guide.md index 0e662110a..f8da3d8b8 100644 --- a/Refactor-migration-guide.md +++ b/Refactor-migration-guide.md @@ -201,6 +201,8 @@ This is a builtin feature of IF, meaning it does not have to be initialized as a - `metrics`: which metrics do you want to aggregate? Every metric you provide here must exist in the output array. + - `method`: the aggregation method for the specied metric + - `type`: the options are `horizontal`, `vertical` or both. Horizontal aggregation is the type that condenses each time series into a single summary value. Vertical aggregation is aggregated across components. Here's what the config block should look like: @@ -208,7 +210,8 @@ Here's what the config block should look like: ```yaml aggregation: metrics: - - 'carbon' + 'carbon': + method: 'sum' type: 'both' ``` diff --git a/manifests/examples/pipelines/nesting.yml b/manifests/examples/pipelines/nesting.yml index ac413d1c6..9f034092f 100644 --- a/manifests/examples/pipelines/nesting.yml +++ b/manifests/examples/pipelines/nesting.yml @@ -6,9 +6,9 @@ tags: category: on-premise aggregation: metrics: - - "carbon" + "carbon": + method: sum type: "both" -params: initialize: plugins: "interpolate": diff --git a/manifests/examples/pipelines/pipeline-with-aggregate.yml b/manifests/examples/pipelines/pipeline-with-aggregate.yml index 7b689f1d8..a833bcb9b 100644 --- a/manifests/examples/pipelines/pipeline-with-aggregate.yml +++ b/manifests/examples/pipelines/pipeline-with-aggregate.yml @@ -3,19 +3,20 @@ description: a full pipeline with the aggregate feature enabled tags: aggregation: metrics: - - "carbon" + "carbon": + method: sum type: "both" initialize: plugins: "interpolate": method: Interpolation - path: 'builtin' + path: "builtin" global-config: method: linear x: [0, 10, 50, 100] y: [0.12, 0.32, 0.75, 1.02] - input-parameter: 'cpu/utilization' - output-parameter: 'cpu-factor' + input-parameter: "cpu/utilization" + output-parameter: "cpu-factor" "cpu-factor-to-wattage": method: Multiply path: builtin @@ -182,4 +183,4 @@ tree: cpu/utilization: 33 cloud/instance-type: A1 cloud/region: uk-west - requests: 180 \ No newline at end of file + requests: 180 diff --git a/manifests/examples/pipelines/pipeline-with-mocks.yml b/manifests/examples/pipelines/pipeline-with-mocks.yml index 2cd23920e..9e4292f51 100644 --- a/manifests/examples/pipelines/pipeline-with-mocks.yml +++ b/manifests/examples/pipelines/pipeline-with-mocks.yml @@ -3,7 +3,8 @@ description: a full pipeline seeded with data from mock-observations feature tags: aggregation: metrics: - - "carbon" + "carbon": + method: sum type: "both" initialize: plugins: diff --git a/manifests/outputs/bugs/aggregation-error-wrong-metric.yaml b/manifests/outputs/bugs/aggregation-error-wrong-metric.yaml index 2bbfba839..ebf55ad73 100644 --- a/manifests/outputs/bugs/aggregation-error-wrong-metric.yaml +++ b/manifests/outputs/bugs/aggregation-error-wrong-metric.yaml @@ -5,7 +5,8 @@ description: >- tags: null aggregation: metrics: - - dummy-param + "dummy-param": + method: sum type: both initialize: plugins: diff --git a/manifests/outputs/features/aggregate-failure-invalid-metrics.yaml b/manifests/outputs/features/aggregate-failure-invalid-metrics.yaml index ad145646f..870be308a 100644 --- a/manifests/outputs/features/aggregate-failure-invalid-metrics.yaml +++ b/manifests/outputs/features/aggregate-failure-invalid-metrics.yaml @@ -2,7 +2,8 @@ name: Aggregation description: Fails with invalid metric. aggregation: metrics: - - test + "test": + method: sum type: both initialize: plugins: diff --git a/manifests/outputs/features/aggregate-failure-missing-metric-in-inputs.yaml b/manifests/outputs/features/aggregate-failure-missing-metric-in-inputs.yaml index 09b2e9338..4a3da85ae 100644 --- a/manifests/outputs/features/aggregate-failure-missing-metric-in-inputs.yaml +++ b/manifests/outputs/features/aggregate-failure-missing-metric-in-inputs.yaml @@ -2,7 +2,8 @@ name: Aggregation description: Fails with missing metric in inputs. aggregation: metrics: - - cpu/utilization + "cpu/utilization": + method: sum type: both initialize: plugins: diff --git a/manifests/outputs/features/aggregate-horizontal.yaml b/manifests/outputs/features/aggregate-horizontal.yaml index 2241815d2..f870bb4d2 100644 --- a/manifests/outputs/features/aggregate-horizontal.yaml +++ b/manifests/outputs/features/aggregate-horizontal.yaml @@ -2,7 +2,8 @@ name: Aggregation description: Apply `horizontal` aggregation aggregation: metrics: - - cpu/utilization + "cpu/utilization": + method: sum type: horizontal initialize: plugins: diff --git a/manifests/outputs/features/aggregate-vertical.yaml b/manifests/outputs/features/aggregate-vertical.yaml index 0c7b32b6a..0fd5b170a 100644 --- a/manifests/outputs/features/aggregate-vertical.yaml +++ b/manifests/outputs/features/aggregate-vertical.yaml @@ -2,7 +2,8 @@ name: Aggregation description: Apply `vertical` aggregation aggregation: metrics: - - cpu/utilization + "cpu/utilization": + method: sum type: vertical initialize: plugins: diff --git a/manifests/outputs/features/aggregate.yaml b/manifests/outputs/features/aggregate.yaml index b58ac9031..35ab21423 100644 --- a/manifests/outputs/features/aggregate.yaml +++ b/manifests/outputs/features/aggregate.yaml @@ -2,7 +2,8 @@ name: Aggregation description: Apply both `horizontal` and `vertical` aggregations aggregation: metrics: - - cpu/utilization + "cpu/utilization": + method: sum type: both initialize: plugins: diff --git a/src/__mocks__/builtins/export-yaml.ts b/src/__mocks__/builtins/export-yaml.ts index 399bbdb28..85f54e966 100644 --- a/src/__mocks__/builtins/export-yaml.ts +++ b/src/__mocks__/builtins/export-yaml.ts @@ -162,6 +162,6 @@ export const aggregated = { }; export const aggregation = { - metrics: ['carbon'], + metrics: {carbon: {method: 'sum'}}, type: 'both', }; diff --git a/src/__mocks__/fs/index.ts b/src/__mocks__/fs/index.ts index 6d4f61b8b..0f37a4291 100644 --- a/src/__mocks__/fs/index.ts +++ b/src/__mocks__/fs/index.ts @@ -10,27 +10,7 @@ export const readFile = async (filePath: string) => { return fs.readFileSync(updatedPath, 'utf8'); } - /** mock for util/json */ - if (filePath.includes('json-reject')) { - return Promise.reject(new Error('rejected')); - } - if (filePath.includes('json')) { - if (filePath.includes('param')) { - return JSON.stringify({ - 'mock-carbon': { - description: 'an amount of carbon emitted into the atmosphere', - unit: 'gCO2e', - aggregation: 'sum', - }, - 'mock-cpu': { - description: 'number of cores available', - unit: 'cores', - aggregation: 'none', - }, - }); - } - return JSON.stringify(filePath); } diff --git a/src/__mocks__/json.ts b/src/__mocks__/json.ts deleted file mode 100644 index d6bb24d2c..000000000 --- a/src/__mocks__/json.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const readAndParseJson = async () => { - return { - 'mock-carbon': { - description: 'an amount of carbon emitted into the atmosphere', - unit: 'gCO2e', - aggregation: 'sum', - }, - 'mock-cpu': { - description: 'number of cores available', - unit: 'cores', - aggregation: 'none', - }, - }; -}; diff --git a/src/__tests__/common/lib/load.test.ts b/src/__tests__/common/lib/load.test.ts index a76d3bd27..0c147271f 100644 --- a/src/__tests__/common/lib/load.test.ts +++ b/src/__tests__/common/lib/load.test.ts @@ -1,6 +1,3 @@ -jest.mock('../../../if-run/util/json', () => - require('../../../__mocks__/json') -); jest.mock( 'mockavizta', () => ({ @@ -42,45 +39,17 @@ jest.mock('../../../common/util/yaml', () => ({ import {PluginParams} from '@grnsft/if-core/types'; -import {PARAMETERS} from '../../../if-run/config'; import {load} from '../../../common/lib/load'; describe('lib/load: ', () => { describe('load(): ', () => { - it('loads yaml with default parameters.', async () => { - const inputPath = 'load-default.yml'; - const paramPath = undefined; - - const result = await load(inputPath, paramPath); - - const expectedValue = { - rawManifest: 'raw-manifest', - parameters: PARAMETERS, - }; - - expect(result).toEqual(expectedValue); - }); - - it('loads yaml with custom parameters.', async () => { + it('successfully loads yaml.', async () => { const inputPath = 'load-default.yml'; - const paramPath = 'param-mock.json'; - const result = await load(inputPath, paramPath); + const result = await load(inputPath); const expectedValue = { rawManifest: 'raw-manifest', - parameters: { - 'mock-carbon': { - description: 'an amount of carbon emitted into the atmosphere', - unit: 'gCO2e', - aggregation: 'sum', - }, - 'mock-cpu': { - description: 'number of cores available', - unit: 'cores', - aggregation: 'none', - }, - }, }; expect(result).toEqual(expectedValue); diff --git a/src/__tests__/if-diff/lib/load.test.ts b/src/__tests__/if-diff/lib/load.test.ts index 8a703df76..cd6fbb1ea 100644 --- a/src/__tests__/if-diff/lib/load.test.ts +++ b/src/__tests__/if-diff/lib/load.test.ts @@ -1,6 +1,3 @@ -jest.mock('../../../if-run/util/json', () => - require('../../../__mocks__/json') -); jest.mock( 'mockavizta', () => ({ diff --git a/src/__tests__/if-run/builtins/time-sync.test.ts b/src/__tests__/if-run/builtins/time-sync.test.ts index ab58b3e34..e216690df 100644 --- a/src/__tests__/if-run/builtins/time-sync.test.ts +++ b/src/__tests__/if-run/builtins/time-sync.test.ts @@ -1,6 +1,9 @@ import {ERRORS} from '@grnsft/if-core/utils'; import {Settings, DateTime} from 'luxon'; +import {AggregationParams} from '../../../common/types/manifest'; + +import {storeAggregateMetrics} from '../../../if-run/lib/aggregate'; import {TimeSync} from '../../../if-run/builtins/time-sync'; import {STRINGS} from '../../../if-run/config'; @@ -51,6 +54,20 @@ jest.mock('luxon', () => { }); describe('builtins/time-sync:', () => { + beforeAll(() => { + const metricStorage: AggregationParams = { + metrics: { + carbon: {method: 'sum'}, + 'cpu/utilization': {method: 'sum'}, + 'time-reserved': {method: 'avg'}, + 'resources-total': {method: 'none'}, + }, + type: 'horizontal', + }; + + storeAggregateMetrics(metricStorage); + }); + describe('time-sync: ', () => { const basicConfig = { 'start-time': '2023-12-12T00:01:00.000Z', diff --git a/src/__tests__/if-run/lib/aggregate.test.ts b/src/__tests__/if-run/lib/aggregate.test.ts index 76de6300c..56488eea2 100644 --- a/src/__tests__/if-run/lib/aggregate.test.ts +++ b/src/__tests__/if-run/lib/aggregate.test.ts @@ -1,8 +1,21 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import {aggregate} from '../../../if-run/lib/aggregate'; +import {AggregationParams} from '../../../common/types/manifest'; + +import {aggregate, storeAggregateMetrics} from '../../../if-run/lib/aggregate'; describe('lib/aggregate: ', () => { + beforeAll(() => { + const metricStorage: AggregationParams = { + metrics: { + carbon: {method: 'sum'}, + }, + type: 'horizontal', + }; + + storeAggregateMetrics(metricStorage); + }); + describe('aggregate(): ', () => { it('returns tree if aggregation is missing.', () => { const tree = {}; @@ -44,7 +57,7 @@ describe('lib/aggregate: ', () => { }; const aggregatedTree = aggregate(tree, { - metrics: ['carbon'], + metrics: {carbon: {method: 'sum'}}, type: 'horizontal', }); const expectedAggregated = { @@ -92,7 +105,7 @@ describe('lib/aggregate: ', () => { }; const aggregatedTree = aggregate(tree, { - metrics: ['carbon'], + metrics: {carbon: {method: 'sum'}}, type: 'vertical', }); const expectedOutputs = [ @@ -153,7 +166,7 @@ describe('lib/aggregate: ', () => { }; const aggregatedTree = aggregate(tree, { - metrics: ['carbon'], + metrics: {carbon: {method: 'sum'}}, type: 'both', }); diff --git a/src/__tests__/if-run/lib/parameterize.test.ts b/src/__tests__/if-run/lib/parameterize.test.ts deleted file mode 100644 index ed3a4b948..000000000 --- a/src/__tests__/if-run/lib/parameterize.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import {LeveledLogMethod} from 'winston'; - -const mockLog = jest.fn((message: string) => message); - -jest.mock('../../../if-run/util/log-memoize', () => ({ - memoizedLog: mockLog, -})); -jest.mock('../../../common/util/logger', () => ({ - logger: { - warn: mockLog, - debug: mockLog, - }, -})); - -import {PARAMETERS} from '../../../if-run/config'; -import {parameterize} from '../../../if-run/lib/parameterize'; - -import {STRINGS} from '../../../if-run/config'; - -import {ManifestParameter} from '../../../common/types/manifest'; - -const {REJECTING_OVERRIDE, CHECKING_AGGREGATION_METHOD} = STRINGS; - -describe('lib/parameterize: ', () => { - afterEach(() => { - (mockLog as jest.Mock).mockReset(); - }); - - describe('getAggregationMethod(): ', () => { - it('returns method for average aggregation method metric.', () => { - const metric = 'cpu/utilization'; - const method = parameterize.getAggregationMethod(metric); - - const expectedMethod = 'avg'; - - expect(method).toEqual(expectedMethod); - }); - - it('returns method for unknown aggregation method metric.', () => { - const metric = 'mock/metric'; - const method = parameterize.getAggregationMethod(metric); - - const expectedMethod = 'sum'; - - expect(method).toEqual(expectedMethod); - expect(mockLog as unknown as LeveledLogMethod).toHaveBeenCalledTimes(2); - }); - - it('prints debug log for first input.', () => { - const unitName = 'timestamp'; - - parameterize.getAggregationMethod(unitName); - - expect(mockLog as typeof console.debug).toHaveBeenCalledWith( - console.debug, - CHECKING_AGGREGATION_METHOD(unitName) - ); - }); - }); - - describe('combine(): ', () => { - it('checks if return type is undefined.', () => { - const params = {}; - const response = parameterize.combine(null, params); - - expect(response).toBeUndefined(); - }); - - it('checks if uninitialized custom param is requested, then returns fallback `sum` method.', () => { - const name = 'mock-name'; - const method = parameterize.getAggregationMethod(name); - - const expectedMethodName = 'sum'; - expect(method).toEqual(expectedMethodName); - }); - - it('checks if custom params are inserted successfully.', () => { - const params = [ - { - name: 'mock-name', - description: 'mock-description', - unit: 'mock/sq', - aggregation: 'none', - }, - ] as ManifestParameter[]; - const object = {}; - - parameterize.combine(params, object); - const method = parameterize.getAggregationMethod(params[0].name); - - expect(method).toEqual(params[0].aggregation); - }); - - it('rejects on default param override.', () => { - const params = [ - { - name: 'carbon', - description: 'mock-description', - unit: 'mock/co', - aggregation: 'none', - }, - ] as ManifestParameter[]; - - parameterize.combine(params, PARAMETERS); - const method = parameterize.getAggregationMethod(params[0].name); - - const expectedMethodName = 'sum'; - const expectedMessage = REJECTING_OVERRIDE(params[0]); - - expect(method).toEqual(expectedMethodName); - expect(mockLog).toHaveBeenCalledWith(expectedMessage); - }); - }); -}); diff --git a/src/__tests__/if-run/util/aggregation-helper.test.ts b/src/__tests__/if-run/util/aggregation-helper.test.ts index 78c8c4aeb..5f8bc3325 100644 --- a/src/__tests__/if-run/util/aggregation-helper.test.ts +++ b/src/__tests__/if-run/util/aggregation-helper.test.ts @@ -1,7 +1,11 @@ import {ERRORS} from '@grnsft/if-core/utils'; import {PluginParams} from '@grnsft/if-core/types'; +import {AggregationParams} from '../../../common/types/manifest'; + import {aggregateInputsIntoOne} from '../../../if-run/util/aggregation-helper'; +import {AggregationMetric} from '../../../if-run/types/aggregation'; +import {storeAggregateMetrics} from '../../../if-run/lib/aggregate'; import {STRINGS} from '../../../if-run/config'; @@ -9,10 +13,23 @@ const {InvalidAggregationMethodError, MissingAggregationParamError} = ERRORS; const {INVALID_AGGREGATION_METHOD, METRIC_MISSING} = STRINGS; describe('util/aggregation-helper: ', () => { + beforeAll(() => { + const metricStorage: AggregationParams = { + metrics: { + carbon: {method: 'sum'}, + 'cpu/number-cores': {method: 'none'}, + 'cpu/utilization': {method: 'sum'}, + }, + type: 'horizontal', + }; + + storeAggregateMetrics(metricStorage); + }); + describe('aggregateInputsIntoOne(): ', () => { it('throws error if aggregation method is none.', () => { const inputs: PluginParams[] = []; - const metrics: string[] = ['cpu/number-cores']; + const metrics: AggregationMetric = {'cpu/number-cores': {method: 'none'}}; const isTemporal = false; expect.assertions(2); @@ -23,14 +40,16 @@ describe('util/aggregation-helper: ', () => { expect(error).toBeInstanceOf(InvalidAggregationMethodError); if (error instanceof InvalidAggregationMethodError) { - expect(error.message).toEqual(INVALID_AGGREGATION_METHOD(metrics[0])); + expect(error.message).toEqual( + INVALID_AGGREGATION_METHOD('cpu/number-cores') + ); } } }); it('throws error if aggregation criteria is not found in input.', () => { const inputs: PluginParams[] = [{timestamp: '', duration: 10}]; - const metrics: string[] = ['cpu/utilization']; + const metrics: AggregationMetric = {'cpu/utilization': {method: 'sum'}}; const isTemporal = false; expect.assertions(2); @@ -41,7 +60,7 @@ describe('util/aggregation-helper: ', () => { expect(error).toBeInstanceOf(MissingAggregationParamError); if (error instanceof MissingAggregationParamError) { - expect(error.message).toEqual(METRIC_MISSING(metrics[0], 0)); + expect(error.message).toEqual(METRIC_MISSING('cpu/utilization', 0)); } } }); @@ -51,7 +70,7 @@ describe('util/aggregation-helper: ', () => { {timestamp: '', duration: 10, carbon: 10}, {timestamp: '', duration: 10, carbon: 20}, ]; - const metrics: string[] = ['carbon']; + const metrics: AggregationMetric = {carbon: {method: 'sum'}}; const isTemporal = true; const expectedValue = { @@ -68,7 +87,7 @@ describe('util/aggregation-helper: ', () => { {timestamp: '', duration: 10, carbon: 10}, {timestamp: '', duration: 10, carbon: 20}, ]; - const metrics: string[] = ['carbon']; + const metrics: AggregationMetric = {carbon: {method: 'sum'}}; const isTemporal = false; const expectedValue = { @@ -79,11 +98,19 @@ describe('util/aggregation-helper: ', () => { }); it('calculates average of metrics.', () => { + const metricStorage: AggregationParams = { + metrics: { + 'cpu/utilization': {method: 'avg'}, + }, + type: 'horizontal', + }; + + storeAggregateMetrics(metricStorage); const inputs: PluginParams[] = [ {timestamp: '', duration: 10, 'cpu/utilization': 10}, {timestamp: '', duration: 10, 'cpu/utilization': 90}, ]; - const metrics: string[] = ['cpu/utilization']; + const metrics: AggregationMetric = {'cpu/utilization': {method: 'avg'}}; const isTemporal = false; const expectedValue = { diff --git a/src/__tests__/if-run/util/args.test.ts b/src/__tests__/if-run/util/args.test.ts index 5ea3dd03b..c43d49eb9 100644 --- a/src/__tests__/if-run/util/args.test.ts +++ b/src/__tests__/if-run/util/args.test.ts @@ -21,11 +21,6 @@ jest.mock('ts-command-line-args', () => ({ manifest: 'manifest-mock.yml', output: 'output-mock.yml', }; - case 'override-params': - return { - manifest: 'manifest-mock.yml', - 'override-params': 'override-params-mock.yml', - }; case 'not-yaml': return { manifest: 'mock.notyaml', @@ -119,22 +114,6 @@ describe('if-run/util/args: ', () => { expect(result).toEqual(expectedResult); }); - it('returns manifest with `paramPath`.', () => { - expect.assertions(1); - - process.env.result = 'override-params'; - - const result = parseIfRunProcessArgs(); - const manifestPath = 'manifest-mock.yml'; - const expectedResult = { - inputPath: path.normalize(`${processRunningPath}/${manifestPath}`), - paramPath: 'override-params-mock.yml', - outputOptions: {}, - }; - - expect(result).toEqual(expectedResult); - }); - it('returns manifest and output path.', () => { expect.assertions(1); diff --git a/src/__tests__/if-run/util/json.test.ts b/src/__tests__/if-run/util/json.test.ts deleted file mode 100644 index 5836c677a..000000000 --- a/src/__tests__/if-run/util/json.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -jest.mock('fs/promises', () => require('../../../__mocks__/fs')); - -import {readAndParseJson} from '../../../if-run/util/json'; - -describe('util/json: ', () => { - describe('readAndParseJson(): ', () => { - it('returns file content from path.', async () => { - const path = 'mock/path/json'; - expect.assertions(1); - - const response = await readAndParseJson(path); - expect(response).toEqual(path); - }); - - it('throws error if path does not exist.', async () => { - const path = 'mock/path/json-reject'; - expect.assertions(1); - - try { - await readAndParseJson(path); - } catch (error) { - expect(error).toBeInstanceOf(Error); - } - }); - }); -}); diff --git a/src/common/lib/load.ts b/src/common/lib/load.ts index 02b44aef1..ade17795d 100644 --- a/src/common/lib/load.ts +++ b/src/common/lib/load.ts @@ -1,30 +1,18 @@ import {openYamlFileAsObject} from '../util/yaml'; -import {readAndParseJson} from '../../if-run/util/json'; -import {PARAMETERS} from '../../if-run/config'; import {STRINGS} from '../../if-run/config'; -import {Parameters} from '../../if-run/types/parameters'; - const {LOADING_MANIFEST} = STRINGS; /** - * Parses manifest file as an object. Checks if parameter file is passed via CLI, then loads it too. - * Returns context, tree and parameters (either the default one, or from CLI). + * Parses manifest file as an object. */ -export const load = async (inputPath: string, paramPath?: string) => { +export const load = async (inputPath: string) => { console.debug(LOADING_MANIFEST); const rawManifest = await openYamlFileAsObject(inputPath); - const parametersFromCli = - paramPath && - (await readAndParseJson(paramPath)); /** @todo validate json */ - const parameters = - parametersFromCli || - PARAMETERS; /** @todo PARAMETERS should be specified in parameterize only */ return { rawManifest, - parameters, }; }; diff --git a/src/common/types/manifest.ts b/src/common/types/manifest.ts index 2359a3433..1b003be2c 100644 --- a/src/common/types/manifest.ts +++ b/src/common/types/manifest.ts @@ -14,5 +14,3 @@ export type AggregationParamsSure = Extract; export type Context = Omit; export type ContextWithExec = Omit; - -export type ManifestParameter = Extract[number]; diff --git a/src/common/util/debug-logger.ts b/src/common/util/debug-logger.ts index 7babaaf3f..35dc77ea7 100644 --- a/src/common/util/debug-logger.ts +++ b/src/common/util/debug-logger.ts @@ -6,7 +6,6 @@ const logMessagesKeys: (keyof typeof STRINGS)[] = [ 'LOADING_MANIFEST', 'VALIDATING_MANIFEST', 'CAPTURING_RUNTIME_ENVIRONMENT_DATA', - 'SYNCING_PARAMETERS', 'CHECKING_AGGREGATION_METHOD', 'INITIALIZING_PLUGINS', 'INITIALIZING_PLUGIN', diff --git a/src/common/util/validations.ts b/src/common/util/validations.ts index a4f0fcb5c..76b785d6d 100644 --- a/src/common/util/validations.ts +++ b/src/common/util/validations.ts @@ -3,8 +3,10 @@ import {ERRORS} from '@grnsft/if-core/utils'; import {STRINGS} from '../../if-run/config'; -import {AGGREGATION_METHODS} from '../../if-run/types/aggregation'; -import {AGGREGATION_TYPES} from '../../if-run/types/parameters'; +import { + AGGREGATION_METHODS, + AGGREGATION_TYPES, +} from '../../if-run/types/aggregation'; const {ManifestValidationError, InputValidationError} = ERRORS; const {VALIDATING_MANIFEST} = STRINGS; @@ -38,22 +40,15 @@ export const manifestSchema = z.object({ .nullable(), aggregation: z .object({ - metrics: z.array(z.string()), - type: z.enum(AGGREGATION_METHODS), + metrics: z.record( + z.object({ + method: z.enum(AGGREGATION_METHODS), + }) + ), + type: z.enum(AGGREGATION_TYPES), }) .optional() .nullable(), - params: z - .array( - z.object({ - name: z.string(), - description: z.string(), - aggregation: z.enum(AGGREGATION_TYPES), - unit: z.string(), - }) - ) - .optional() - .nullable(), initialize: z.object({ plugins: z.record( z.string(), diff --git a/src/if-run/builtins/README.md b/src/if-run/builtins/README.md index df743e93c..ccacf83e5 100644 --- a/src/if-run/builtins/README.md +++ b/src/if-run/builtins/README.md @@ -165,7 +165,7 @@ Note that when `error-on-padding` is `true` no padding is performed and the plug ##### Resampling rules -Now we have synchronized, continuous, high resolution time series data, we can resample. To achieve this, we use `interval`, which sets the global temporal resolution for the final, processed time series. `intervalk` is expressed in units of seconds, which means we can simply batch `observations` together in groups of size `interval`. For each value in each object we either sum, average or copy the values into one single summary object representing each time bucket of size `interval` depending on their `aggregation-method` defined in `params.ts`. The returned array is the final, synchronized time series at the desired temporal resolution. +Now we have synchronized, continuous, high resolution time series data, we can resample. To achieve this, we use `interval`, which sets the global temporal resolution for the final, processed time series. `interval` is expressed in units of seconds, which means we can simply batch `observations` together in groups of size `interval`. For each value in each object we either sum, average or copy the values into one single summary object representing each time bucket of size `interval` depending on their `aggregation-method` defined in `aggregation` section in the manifest file. The returned array is the final, synchronized time series at the desired temporal resolution. #### Assumptions and limitations diff --git a/src/if-run/builtins/time-sync.ts b/src/if-run/builtins/time-sync.ts index 78b293e75..9c14b63a4 100644 --- a/src/if-run/builtins/time-sync.ts +++ b/src/if-run/builtins/time-sync.ts @@ -11,11 +11,10 @@ import { TimeParams, } from '@grnsft/if-core/types'; -import {parameterize} from '../lib/parameterize'; - import {validate} from '../../common/util/validations'; import {STRINGS} from '../config'; +import {getAggregationMethod} from '../lib/aggregate'; Settings.defaultZone = 'utc'; @@ -200,7 +199,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { const inputKeys = Object.keys(input); return inputKeys.reduce((acc, key) => { - const method = parameterize.getAggregationMethod(key); + const method = getAggregationMethod(key); if (key === 'timestamp') { const perSecond = normalizeTimePerSecond(input.timestamp, i); @@ -254,7 +253,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { return acc; } - const method = parameterize.getAggregationMethod(metric); + const method = getAggregationMethod(metric); if (method === 'avg' || method === 'sum') { acc[metric] = 0; @@ -313,7 +312,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { const metrics = Object.keys(input); metrics.forEach(metric => { - const method = parameterize.getAggregationMethod(metric); + const method = getAggregationMethod(metric); acc[metric] = acc[metric] ?? 0; if (metric === 'timestamp') { diff --git a/src/if-run/config/config.ts b/src/if-run/config/config.ts index 99ca34193..91f366543 100644 --- a/src/if-run/config/config.ts +++ b/src/if-run/config/config.ts @@ -21,12 +21,6 @@ export const CONFIG = { alias: 'o', description: '[path to the output file]', }, - 'override-params': { - type: String, - optional: true, - alias: 'p', - description: '[path to a parameter file that overrides our defaults]', - }, 'no-output': { type: Boolean, optional: true, diff --git a/src/if-run/config/index.ts b/src/if-run/config/index.ts index 6aa6e0a98..4972b390b 100644 --- a/src/if-run/config/index.ts +++ b/src/if-run/config/index.ts @@ -1,3 +1,2 @@ export {CONFIG} from './config'; -export {PARAMETERS} from './params'; export {STRINGS} from './strings'; diff --git a/src/if-run/config/params.ts b/src/if-run/config/params.ts deleted file mode 100644 index 76732e57b..000000000 --- a/src/if-run/config/params.ts +++ /dev/null @@ -1,198 +0,0 @@ -import {Parameters} from '../types/parameters'; - -export const PARAMETERS: Parameters = { - carbon: { - description: 'an amount of carbon emitted into the atmosphere', - unit: 'gCO2e', - aggregation: 'sum', - }, - 'cpu/number-cores': { - description: 'number of cores available', - unit: 'cores', - aggregation: 'none', - }, - 'cpu/utilization': { - description: 'refers to CPU utilization.', - unit: 'percentage', - aggregation: 'avg', - }, - 'disk-io': { - description: 'refers to GB of data written/read from disk', - unit: 'GB', - aggregation: 'sum', - }, - duration: { - description: 'refers to the duration of the input', - unit: 'seconds', - aggregation: 'sum', - }, - energy: { - description: 'amount of energy utilised by the component', - unit: 'kWh', - aggregation: 'sum', - }, - 'cpu/energy': { - description: 'Energy consumed by the CPU of the component', - unit: 'kWh', - aggregation: 'sum', - }, - 'device/expected-lifespan': { - description: 'Total Expected Lifespan of the Component in Seconds', - unit: 'seconds', - aggregation: 'sum', - }, - 'memory/energy': { - description: 'Energy consumed by the Memory of the component', - unit: 'kWh', - aggregation: 'sum', - }, - 'carbon-embodied': { - description: 'Embodied Emissions of the component', - unit: 'gCO2e', - aggregation: 'sum', - }, - 'network/energy': { - description: 'Energy consumed by the Network of the component', - unit: 'kWh', - aggregation: 'sum', - }, - 'functional-unit': { - description: - 'the name of the functional unit in which the final SCI value should be expressed, e.g. requests, users', - unit: 'none', - aggregation: 'sum', - }, - 'gpu-util': { - description: 'refers to CPU utilization.', - unit: 'percentage', - aggregation: 'avg', - }, - 'grid/carbon-intensity': { - description: 'Carbon intensity for the grid', - unit: 'gCO2eq/kWh', - aggregation: 'avg', - }, - 'cloud/instance-type': { - description: 'Type of Cloud Instance name used in the cloud provider APIs', - unit: 'None', - aggregation: 'none', - }, - geolocation: { - description: - 'Geographic location of provider as string (for watt-time model it is provided as latitude and longitude, comma separated, in decimal degrees)', - unit: 'None (decimal degrees for watt-time model)', - aggregation: 'none', - }, - 'carbon-operational': { - description: 'Operational Emissions of the component', - unit: 'gCO2e', - aggregation: 'sum', - }, - 'cpu/name': { - description: 'Name of the physical processor', - unit: 'None', - aggregation: 'none', - }, - 'cloud/region': { - description: 'region cloud instance runs in', - unit: 'None', - aggregation: 'none', - }, - 'cloud/vendor': { - description: - 'Name of the cloud service provider in the ccf model. Can be aws, gcp or azure', - unit: 'None', - aggregation: 'none', - }, - name: { - description: 'arbitrary name parameter.', - unit: 'None', - aggregation: 'none', - }, - 'ram-alloc': { - description: 'refers to GB of memory allocated.', - unit: 'GB', - aggregation: 'avg', - }, - 'ram-util': { - description: 'refers to percentage of memory utilized.', - unit: 'percentage', - aggregation: 'avg', - }, - 'resources-reserved': { - description: 'resources reserved for an application', - unit: 'count', - aggregation: 'none', - }, - 'cpu/thermal-design-power': { - description: 'thermal design power for a processor', - unit: 'kwh', - aggregation: 'avg', - }, - 'device/emissions-embodied': { - description: 'total embodied emissions of some component', - unit: 'gCO2e', - aggregation: 'sum', - }, - timestamp: { - description: 'refers to the time of occurrence of the input', - unit: 'RFC3339', - aggregation: 'none', - }, - 'time-reserved': { - description: 'time reserved for a component', - unit: 'seconds', - aggregation: 'avg', - }, - 'resources-total': { - description: 'total resources available', - unit: 'count', - aggregation: 'none', - }, - 'vcpus-allocated': { - description: 'number of vcpus allocated to particular resource', - unit: 'count', - aggregation: 'none', - }, - 'vcpus-total': { - description: 'total number of vcpus available on a particular resource', - unit: 'count', - aggregation: 'none', - }, - 'memory-available': { - description: 'total amount of memory available on a particular resource', - unit: 'GB', - aggregation: 'none', - }, - 'physical-processor': { - description: - 'name of the physical processor being used in a specific instance type', - unit: 'None', - aggregation: 'none', - }, - 'cloud/region-cfe': { - description: 'cloud region name in cfe format', - unit: 'None', - aggregation: 'none', - }, - 'cloud/region-em-zone-id': { - description: 'cloud region name in electricity maps format', - unit: 'None', - aggregation: 'none', - }, - 'cloud/region-wt-id': { - description: 'cloud region name in watt-time format', - unit: 'None', - aggregation: 'none', - }, - 'cloud/region-location': { - description: 'cloud region name in our IF format', - unit: 'None', - aggregation: 'none', - }, - 'cloud/region-geolocation': { - description: 'location expressed as decimal coordinates (lat/lon)', - unit: 'decimal degrees', - aggregation: 'none', - }, -}; diff --git a/src/if-run/config/strings.ts b/src/if-run/config/strings.ts index 5f5387c1f..cdbc7c8c2 100644 --- a/src/if-run/config/strings.ts +++ b/src/if-run/config/strings.ts @@ -1,12 +1,8 @@ -import {ManifestParameter} from '../../common/types/manifest'; - export const STRINGS = { MISSING_METHOD: "Initalization param 'method' is missing.", MISSING_PATH: "Initalization param 'path' is missing.", UNSUPPORTED_PLUGIN: "Plugin interface doesn't implement 'execute' or 'metadata' methods.", - OVERRIDE_WARNING: - '\n**WARNING**: You are overriding the IF default parameters file. Please be extremely careful of unintended side-effects in your plugin pipeline!\n', NOT_NATIVE_PLUGIN: (path: string) => ` You are using plugin ${path} which is not part of the Impact Framework standard library. You should do your own research to ensure the plugins are up to date and accurate. They may not be actively maintained.`, @@ -28,8 +24,6 @@ export const STRINGS = { METRIC_MISSING: (metric: string, index: number) => `Aggregation metric ${metric} is not found in inputs[${index}].`, INVALID_GROUP_BY: (type: string) => `Invalid group ${type}.`, - REJECTING_OVERRIDE: (param: ManifestParameter) => - `Rejecting overriding of canonical parameter: ${param.name}.`, INVALID_EXHAUST_PLUGIN: (pluginName: string) => `Invalid exhaust plugin: ${pluginName}.`, UNKNOWN_PARAM: (name: string) => @@ -48,7 +42,6 @@ Note that for the '--output' option you also need to define the output type in y LOADING_MANIFEST: 'Loading manifest', VALIDATING_MANIFEST: 'Validating manifest', CAPTURING_RUNTIME_ENVIRONMENT_DATA: 'Capturing runtime environment data', - SYNCING_PARAMETERS: 'Syncing parameters', CHECKING_AGGREGATION_METHOD: (unitName: string) => `Checking aggregation method for ${unitName}`, INITIALIZING_PLUGINS: 'Initializing plugins', diff --git a/src/if-run/index.ts b/src/if-run/index.ts index fa5d17fa2..5ffb62ded 100644 --- a/src/if-run/index.ts +++ b/src/if-run/index.ts @@ -1,10 +1,9 @@ #!/usr/bin/env node -import {aggregate} from './lib/aggregate'; +import {aggregate, storeAggregateMetrics} from './lib/aggregate'; import {compute} from './lib/compute'; import {injectEnvironment} from './lib/environment'; import {exhaust} from './lib/exhaust'; import {initialize} from './lib/initialize'; -import {parameterize} from './lib/parameterize'; import {load} from '../common/lib/load'; import {parseIfRunProcessArgs} from './util/args'; @@ -21,19 +20,22 @@ const {DISCLAIMER_MESSAGE} = COMMON_STRINGS; const impactEngine = async () => { const options = parseIfRunProcessArgs(); - const {inputPath, paramPath, outputOptions, debug} = options; + const {inputPath, outputOptions, debug} = options; debugLogger.overrideConsoleMethods(!!debug); logger.info(DISCLAIMER_MESSAGE); console.info(STARTING_IF); - const {rawManifest, parameters} = await load(inputPath, paramPath); + const {rawManifest} = await load(inputPath); const envManifest = await injectEnvironment(rawManifest); try { const {tree, ...context} = validateManifest(envManifest); - parameterize.combine(context.params, parameters); + + // TODO: remove this after resolving timeSync to be a builtin functionality. + storeAggregateMetrics(context.aggregation); + const pluginStorage = await initialize(context.initialize.plugins); const computedTree = await compute(tree, {context, pluginStorage}); const aggregatedTree = aggregate(computedTree, context.aggregation); diff --git a/src/if-run/lib/aggregate.ts b/src/if-run/lib/aggregate.ts index 4c16a0d0d..a3d2d2bad 100644 --- a/src/if-run/lib/aggregate.ts +++ b/src/if-run/lib/aggregate.ts @@ -1,14 +1,25 @@ import {PluginParams} from '@grnsft/if-core/types'; -import {aggregateInputsIntoOne} from '../util/aggregation-helper'; - -import {STRINGS} from '../config/strings'; +import {debugLogger} from '../../common/util/debug-logger'; +import {logger} from '../../common/util/logger'; import { AggregationParams, AggregationParamsSure, } from '../../common/types/manifest'; -const {AGGREGATING_NODE, AGGREGATING_OUTPUTS} = STRINGS; +import {aggregateInputsIntoOne} from '../util/aggregation-helper'; +import {memoizedLog} from '../util/log-memoize'; + +import {AggregationMetric} from '../types/aggregation'; + +import {STRINGS} from '../config/strings'; + +const { + AGGREGATING_NODE, + AGGREGATING_OUTPUTS, + UNKNOWN_PARAM, + CHECKING_AGGREGATION_METHOD, +} = STRINGS; /** * Gets `i`th element from all children outputs and collects them in single array. @@ -27,7 +38,7 @@ const getIthElementsFromChildren = (children: any, i: number) => { * 1. Gets the i'th element from each childrens outputs (treating children as rows and we are after a column of data). * 2. Now we just aggregate over the `ithSliceOfOutputs` the same as we did for the normal outputs. */ -const temporalAggregation = (node: any, metrics: string[]) => { +const temporalAggregation = (node: any, metrics: AggregationMetric) => { const outputs: PluginParams[] = []; const values: any = Object.values(node.children); @@ -91,3 +102,52 @@ export const aggregate = (tree: any, aggregationParams: AggregationParams) => { return copyOfTree; }; + +/** + * Gets or stores aggregation metrics. + * @todo Remove these functions after resolving timeSync to be a builtin functionality. + */ +export const storeAggregateMetrics = ( + aggregationParams?: AggregationParams +) => { + if (aggregationParams?.metrics) { + metricManager.metrics = aggregationParams?.metrics; + } + + return metricManager.metrics; +}; + +/** + * Creates an encapsulated object to retrieve the metrics. + */ +const metricManager = (() => { + let metric: AggregationMetric; + + const manager = { + get metrics() { + return metric; + }, + set metrics(value: AggregationMetric) { + metric = value; + }, + }; + + return manager; +})(); + +/** + * Returns aggregation method for given `unitName`. If doesn't exist then returns value `sum`. + */ +export const getAggregationMethod = (unitName: string) => { + debugLogger.setExecutingPluginName(); + memoizedLog(console.debug, CHECKING_AGGREGATION_METHOD(unitName)); + const aggregationMetricsStorage = storeAggregateMetrics(); + + if (aggregationMetricsStorage && `${unitName}` in aggregationMetricsStorage) { + return aggregationMetricsStorage[unitName].method; + } + + memoizedLog(logger.warn, UNKNOWN_PARAM(unitName)); + + return 'sum'; +}; diff --git a/src/if-run/lib/parameterize.ts b/src/if-run/lib/parameterize.ts deleted file mode 100644 index 1034e1ce2..000000000 --- a/src/if-run/lib/parameterize.ts +++ /dev/null @@ -1,79 +0,0 @@ -import {debugLogger} from '../../common/util/debug-logger'; -import {logger} from '../../common/util/logger'; -import {memoizedLog} from '../util/log-memoize'; - -import {STRINGS, PARAMETERS} from '../config'; - -import {Parameters} from '../types/parameters'; -import {ManifestParameter} from '../../common/types/manifest'; - -const { - REJECTING_OVERRIDE, - UNKNOWN_PARAM, - SYNCING_PARAMETERS, - CHECKING_AGGREGATION_METHOD, -} = STRINGS; - -/** - * Parameters manager. Provides get aggregation method and combine functionality. - */ -const Parameterize = () => { - let parametersStorage = PARAMETERS; - - /** - * Returns aggregation method for given `unitName`. If doesn't exist then returns value `sum`. - */ - const getAggregationMethod = (unitName: string) => { - debugLogger.setExecutingPluginName(); - memoizedLog(console.debug, CHECKING_AGGREGATION_METHOD(unitName)); - - if (`${unitName}` in parametersStorage) { - return parametersStorage[unitName as keyof typeof PARAMETERS].aggregation; - } - - memoizedLog(logger.warn, UNKNOWN_PARAM(unitName)); - - return 'sum'; - }; - - /** - * Checks if additional parameters are provided in context. - * If so, then checks if they are coincident with default ones and exits with warning message. - * Otherwise appends context based parameters to defaults. - */ - const combine = ( - contextParameters: ManifestParameter[] | null | undefined, - parameters: Parameters - ) => { - console.debug(SYNCING_PARAMETERS); - - if (contextParameters) { - contextParameters.forEach(param => { - if (`${param.name}` in parameters) { - logger.warn(REJECTING_OVERRIDE(param)); - - return; - } - - const {description, unit, aggregation, name} = param; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - parameters[name] = { - description, - unit, - aggregation, - }; - }); - } - - parametersStorage = parameters; - }; - - return { - combine, - getAggregationMethod, - }; -}; - -export const parameterize = Parameterize(); diff --git a/src/if-run/types/aggregation.ts b/src/if-run/types/aggregation.ts index d15322d29..a7e895da4 100644 --- a/src/if-run/types/aggregation.ts +++ b/src/if-run/types/aggregation.ts @@ -1,3 +1,9 @@ export type AggregationResult = Record; -export const AGGREGATION_METHODS = ['horizontal', 'vertical', 'both'] as const; +export const AGGREGATION_TYPES = ['horizontal', 'vertical', 'both'] as const; +export const AGGREGATION_METHODS = ['sum', 'avg', 'none'] as const; + +export type AggregationMetric = Record< + string, + {method: 'sum' | 'avg' | 'none'} +>; diff --git a/src/if-run/types/parameters.ts b/src/if-run/types/parameters.ts deleted file mode 100644 index 939ac2ba6..000000000 --- a/src/if-run/types/parameters.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {ManifestParameter} from '../../common/types/manifest'; - -export const AGGREGATION_TYPES = ['sum', 'none', 'avg'] as const; - -type ParameterProps = Omit; - -export type Parameters = Record; diff --git a/src/if-run/types/process-args.ts b/src/if-run/types/process-args.ts index a336b0af0..184877bd0 100644 --- a/src/if-run/types/process-args.ts +++ b/src/if-run/types/process-args.ts @@ -1,7 +1,6 @@ export interface IfRunArgs { manifest?: string; output?: string; - 'override-params'?: string; 'no-output'?: boolean; debug?: boolean; } diff --git a/src/if-run/util/aggregation-helper.ts b/src/if-run/util/aggregation-helper.ts index 408a4e9f7..87d3e15fc 100644 --- a/src/if-run/util/aggregation-helper.ts +++ b/src/if-run/util/aggregation-helper.ts @@ -1,11 +1,11 @@ import {ERRORS} from '@grnsft/if-core/utils'; import {PluginParams} from '@grnsft/if-core/types'; -import {parameterize} from '../lib/parameterize'; - import {CONFIG, STRINGS} from '../config'; -import {AggregationResult} from '../types/aggregation'; +import {AggregationMetric, AggregationResult} from '../types/aggregation'; + +import {getAggregationMethod} from '../lib/aggregate'; const {InvalidAggregationMethodError, MissingAggregationParamError} = ERRORS; const {INVALID_AGGREGATION_METHOD, METRIC_MISSING} = STRINGS; @@ -15,9 +15,9 @@ const {AGGREGATION_ADDITIONAL_PARAMS} = CONFIG; * Validates metrics array before applying aggregator. * If aggregation method is `none`, then throws error. */ -const checkIfMetricsAreValid = (metrics: string[]) => { - metrics.forEach(metric => { - const method = parameterize.getAggregationMethod(metric); +const checkIfMetricsAreValid = (metrics: AggregationMetric) => { + Object.keys(metrics).forEach(metric => { + const method = metrics[metric].method; if (method === 'none') { throw new InvalidAggregationMethodError( @@ -33,11 +33,14 @@ const checkIfMetricsAreValid = (metrics: string[]) => { */ export const aggregateInputsIntoOne = ( inputs: PluginParams[], - metrics: string[], + metrics: AggregationMetric, isTemporal?: boolean ) => { checkIfMetricsAreValid(metrics); - const extendedMetrics = [...metrics, ...AGGREGATION_ADDITIONAL_PARAMS]; + const extendedMetrics = [ + ...Object.keys(metrics), + ...AGGREGATION_ADDITIONAL_PARAMS, + ]; return inputs.reduce((acc, input, index) => { for (const metric of extendedMetrics) { @@ -56,7 +59,7 @@ export const aggregateInputsIntoOne = ( /** Checks for the last iteration. */ if (index === inputs.length - 1) { - if (parameterize.getAggregationMethod(metric) === 'avg') { + if (getAggregationMethod(metric) === 'avg') { acc[metric] /= inputs.length; } } diff --git a/src/if-run/util/args.ts b/src/if-run/util/args.ts index 632a17fd6..140c5d140 100644 --- a/src/if-run/util/args.ts +++ b/src/if-run/util/args.ts @@ -33,7 +33,7 @@ const validateAndParseProcessArgs = () => { }; /** - * 1. Parses process arguments like `manifest`, `output`, `override-params`, `help` and `debug`. + * 1. Parses process arguments like `manifest`, `output`, `help` and `debug`. * 2. Checks if `help` param is provided, then logs help message and exits. * 3. If output params are missing, warns user about it. * 3. Otherwise checks if `manifest` param is there, then processes with checking if it's a yaml file. @@ -44,7 +44,6 @@ export const parseIfRunProcessArgs = (): ProcessArgsOutputs => { const { manifest, output, - 'override-params': overrideParams, 'no-output': noOutput, debug, } = validateAndParseProcessArgs(); @@ -61,7 +60,6 @@ export const parseIfRunProcessArgs = (): ProcessArgsOutputs => { ...(output && {outputPath: prependFullFilePath(output)}), ...(noOutput && {noOutput}), }, - ...(overrideParams && {paramPath: overrideParams}), debug, }; } diff --git a/src/if-run/util/json.ts b/src/if-run/util/json.ts deleted file mode 100644 index c9180a788..000000000 --- a/src/if-run/util/json.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as fs from 'fs/promises'; - -/** - * Reads and parses json file. - */ -export const readAndParseJson = async (paramPath: string): Promise => { - const file = await fs.readFile(paramPath, 'utf-8'); - - return JSON.parse(file) as T; -};