Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1cf6be0
exhaust: added exhaust function, with stub for exhaust-csv-export and…
pazbardanl Feb 18, 2024
c6b8484
exhaust csv exporter: initial implementation, few inline TODOs and al…
pazbardanl Feb 18, 2024
461f4f7
refactor exhaust export csv to comply with functional arch
pazbardanl Feb 18, 2024
22b3c24
Better error and exhaust context validation
pazbardanl Feb 18, 2024
cc3028f
exhaust: added exhaust function, with stub for exhaust-csv-export and…
pazbardanl Feb 18, 2024
39b9852
exhaust csv exporter: initial implementation, few inline TODOs and al…
pazbardanl Feb 18, 2024
15e5b85
refactor exhaust export csv to comply with functional arch
pazbardanl Feb 18, 2024
fc47753
Better error and exhaust context validation
pazbardanl Feb 18, 2024
1304605
added exhaust to validations and manifest load
pazbardanl Feb 19, 2024
5f8a9b1
refactored exhaust-csv-export to fit aggregated tree structure and al…
pazbardanl Feb 20, 2024
1a97268
cleaner code, refactor exhaust to iterate through exhaust plugins ins…
pazbardanl Feb 21, 2024
fcbf885
refactored exhaust-export-csv to fit revised manifast format
pazbardanl Feb 22, 2024
9f7ae1b
code review fixes
pazbardanl Feb 25, 2024
5a95192
chore: update exhaust csv branch
narekhovhannisyan Feb 28, 2024
e0532ce
chore: update branch from functional architecture
narekhovhannisyan Feb 28, 2024
429c876
chore(src): integrate latest aggregation changes
narekhovhannisyan Feb 29, 2024
48863b4
feat(util): make outputs array in validations
narekhovhannisyan Mar 1, 2024
a63cd5e
feat(util): add Make directory error
narekhovhannisyan Mar 1, 2024
a837145
chore(types): make outputs array in manifest
narekhovhannisyan Mar 1, 2024
0971139
feat(types): tune exhaust plugin interface
narekhovhannisyan Mar 1, 2024
10b5e5f
feat(models): implement export yaml
narekhovhannisyan Mar 1, 2024
3e5c4f9
feat(models): implement export log
narekhovhannisyan Mar 1, 2024
2b3154b
chore(models): pretty export csv
narekhovhannisyan Mar 1, 2024
6799541
revert(models): drop unused exhausts
narekhovhannisyan Mar 1, 2024
79d5beb
refactor(lib): rewrite exhaust
narekhovhannisyan Mar 1, 2024
401539a
feat(config): add invalid exhaust plugin to strings
narekhovhannisyan Mar 1, 2024
5c247c8
feat(src): use exhaust as an output transport
narekhovhannisyan Mar 1, 2024
7c99326
feat(config): in commitlintrc add models and plugins as scope
narekhovhannisyan Mar 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .commitlintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ module.exports = {
'examples',
'.github',
'.husky',
'scripts'
'scripts',
'models',
'plugins'
]
],
'scope-empty': [
Expand Down
22 changes: 22 additions & 0 deletions examples/impls/functional/sci-e-exhaust-csv.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: sci-e-demo
description:
tags:
initialize:
plugins:
'sci-e':
model: SciE
path: '@grnsft/if-models'
exhaust:
pipeline: ['yaml', 'csv', 'grafana']
basepath: ''
tree:
children:
child:
pipeline:
- sci-e
config:
sci-e:
inputs:
- timestamp: 2023-08-06T00:00
duration: 3600
energy-cpu: 0.001
55 changes: 55 additions & 0 deletions examples/impls/functional/time-sync-exhaust-csv-export.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: exhaust-csv-export demo
description:
tags:
initialize:
plugins:
'time-sync':
method: TimeSync
path: 'builtin'
global-config:
start-time: '2023-12-12T00:00:00.000Z'
end-time: '2023-12-12T00:01:00.000Z'
interval: 5
allow-padding: true
outputs:
'csv':
output-path: 'C:\dev\demo-exhaust-csv-export.csv'
tree:
children:
child1:
pipeline:
- time-sync
config:
inputs:
- timestamp: '2023-12-12T00:00:00.000Z'
duration: 1
energy-cpu: 0.001
custom-metric: 0.001
- timestamp: '2023-12-12T00:00:01.000Z'
duration: 5
energy-cpu: 0.001
- timestamp: '2023-12-12T00:00:06.000Z'
duration: 7
energy-cpu: 0.001
- timestamp: '2023-12-12T00:00:13.000Z'
duration: 30
energy-cpu: 0.001
custom-metric: 0.002
child2:
pipeline:
- time-sync
config:
inputs:
- timestamp: '2023-12-12T00:00:00.000Z'
duration: 1
energy-cpu: 0.001
- timestamp: '2023-12-12T00:00:01.000Z'
duration: 5
energy-cpu: 0.001
custom-metric: 0.003
- timestamp: '2023-12-12T00:00:06.000Z'
duration: 7
energy-cpu: 0.001
- timestamp: '2023-12-12T00:00:13.000Z'
duration: 30
energy-cpu: 0.001
2 changes: 2 additions & 0 deletions src/config/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ https://github.com/Green-Software-Foundation/if/issues/new?assignees=&labels=fee
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}.`,
};
15 changes: 2 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import {aggregate} from './lib/aggregate';
import {compute} from './lib/compute';
import {exhaust} from './lib/exhaust';
import {initalize} from './lib/initialize';
import {load} from './lib/load';
import {parameterize} from './lib/parameterize';
Expand All @@ -9,7 +10,6 @@ import {parseArgs} from './util/args';
import {ERRORS} from './util/errors';
import {andHandle} from './util/helpers';
import {logger} from './util/logger';
import {saveYamlFileAs} from './util/yaml';

import {STRINGS} from './config';

Expand All @@ -29,18 +29,7 @@ const impactEngine = async () => {
const plugins = await initalize(context.initialize.plugins);
const computedTree = await compute(tree, {context, plugins});
const aggregatedTree = aggregate(computedTree, context.aggregation);

const outputFile = {
...context,
tree: aggregatedTree,
};

if (!outputPath) {
logger.info(JSON.stringify(outputFile, null, 2));
return;
}

await saveYamlFileAs(outputFile, outputPath);
exhaust(aggregatedTree, context, outputPath);

return;
}
Expand Down
55 changes: 55 additions & 0 deletions src/lib/exhaust.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @todo This is temporary solution, will be refactored to support dynamic plugins.
*/
import {ExportCsv} from '../models/export-csv';
import {ExportLog} from '../models/export-log';
import {ExportYaml} from '../models/export-yaml';

import {ERRORS} from '../util/errors';

import {STRINGS} from '../config';

import {ExhaustPluginInterface} from '../types/exhaust-plugin-interface';
import {Context} from '../types/manifest';

const {ModuleInitializationError} = ERRORS;
const {INVALID_EXHAUST_PLUGIN} = STRINGS;

/**
* Initialize exhaust plugins based on the provided config
*/
const initializeExhaustPlugins = (plugins: string[]) =>
plugins.map(initializeExhaustPlugin);

/**
* factory method for exhaust plugins
*/
const initializeExhaustPlugin = (name: string): ExhaustPluginInterface => {
switch (name) {
case 'yaml':
return ExportYaml();
case 'csv':
return ExportCsv();
case 'log':
return ExportLog();
default:
throw new ModuleInitializationError(INVALID_EXHAUST_PLUGIN(name));
}
};

/**
* Output manager - Exhaust.
* Grabs output plugins from context, executes every.
*/
export const exhaust = (tree: any, context: Context, outputPath?: string) => {
const outputPlugins = context.initialize.outputs;

if (!outputPlugins) {
ExportLog().execute(tree, context);

return;
}

const exhaustPlugins = initializeExhaustPlugins(outputPlugins);
exhaustPlugins.forEach(plugin => plugin.execute(tree, context, outputPath));
};
155 changes: 155 additions & 0 deletions src/models/export-csv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import * as fs from 'fs/promises';

import {ERRORS} from '../util/errors';

import {ExhaustPluginInterface} from '../types/exhaust-plugin-interface';
import {Context} from '../types/manifest';

const {WriteFileError, CliInputError} = ERRORS;

export const ExportCsv = (): ExhaustPluginInterface => {
/**
* handle a tree leaf, where there are no child nodes, by adding it as key->value pair to the flat map
* and capturing key as a header
*/
const handleLeafValue = (
value: any,
fullPath: string,
key: any,
flatMap: {[key: string]: any},
headers: Set<string>
) => {
if (fullPath.includes('outputs')) {
headers.add(key);
flatMap[fullPath] = value;
}
};

/**
* handle a tree node, recursively traverse the children and append their results to the flat map and captured headers
*/
const handleNodeValue = (
value: any,
fullPath: string,
flatMap: Record<string, any>,
headers: Set<string>
) => {
const [subFlatMap, subHeaders] = extractFlatMapAndHeaders(value, fullPath);

if (Object.keys(subFlatMap).length > 0) {
Object.entries(subFlatMap).forEach(([subKey, value]) => {
flatMap[subKey] = value;
});

subHeaders.forEach(subHeader => {
headers.add(subHeader);
});
}
};

/**
* Handles a key at the top level of the tree
*/
const handleKey = (
value: any,
key: any,
prefix: string,
flatMap: Record<string, any>,
headers: Set<string>
) => {
const fullPath = prefix ? `${prefix}.${key}` : key;

if (value !== null && typeof value === 'object') {
return handleNodeValue(value, fullPath, flatMap, headers);
}

return handleLeafValue(value, fullPath, key, flatMap, headers);
};

/**
* qrecursively extract a flat map and headers from the hierarcial tree
*/
const extractFlatMapAndHeaders = (
tree: any,
prefix = ''
): [Record<string, any>, Set<string>] => {
const headers: Set<string> = new Set();
const flatMap: Record<string, any> = [];

for (const key in tree) {
if (key in tree) {
handleKey(tree[key], key, prefix, flatMap, headers);
}
}

return [flatMap, headers];
};

/**
* extract the id of the key, that is removing the last token (which is the index).
* in this manner, multiple keys that identical besides their index share the same id.
*/
const extractIdHelper = (key: string): string => {
const parts = key.split('.');
parts.pop();

return parts.join('.');
};

/**
* generate a CSV formatted string based on a flat key->value map, headers and ids
*/
const getCsvString = (
map: {[key: string]: any},
headers: Set<string>,
ids: Set<string>
): string => {
const csvRows: string[] = [];
csvRows.push(['id', ...headers].join(','));

ids.forEach(id => {
const rowData = [id];

headers.forEach(header => {
const value = map[`${id}.${header}`] ?? '';
rowData.push(value.toString());
});
csvRows.push(rowData.join(','));
});

return csvRows.join('\n');
};

/**
* write the given string content to a file at the provided path
*/
const writeOutputFile = async (content: string, outputPath: string) => {
try {
await fs.writeFile(outputPath, content);
} catch (error) {
throw new WriteFileError(
`Failed to write CSV to ${outputPath}: ${error}`
);
}
};

/**
* export the provided tree content to a CSV file, represented in a flat structure
*/
const execute = async (tree: any, _context: Context, outputPath: string) => {
if (!outputPath) {
throw new CliInputError('Output path is required.');
}

const [extractredFlatMap, extractedHeaders] =
extractFlatMapAndHeaders(tree);
const ids = new Set(
Object.keys(extractredFlatMap).map(key => extractIdHelper(key))
);
const csvString = getCsvString(extractredFlatMap, extractedHeaders, ids);

writeOutputFile(csvString, outputPath);
};

return {execute};
};
17 changes: 17 additions & 0 deletions src/models/export-log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Context} from '../types/manifest';

export const ExportLog = () => {
/**
* Logs output manifest in console.
*/
const execute = async (tree: any, context: Context) => {
const outputFile = {
...context,
tree,
};

console.log(JSON.stringify(outputFile, null, 2));
};

return {execute};
};
27 changes: 27 additions & 0 deletions src/models/export-yaml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {saveYamlFileAs} from '../util/yaml';

import {ERRORS} from '../util/errors';

import {Context} from '../types/manifest';

const {CliInputError} = ERRORS;

export const ExportYaml = () => {
/**
* Saves output file in YAML format.
*/
const execute = async (tree: any, context: Context, outputPath: string) => {
if (!outputPath) {
throw new CliInputError('Output path is required.');
}

const outputFile = {
...context,
tree,
};

await saveYamlFileAs(outputFile, outputPath);
};

return {execute};
};
1 change: 1 addition & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export {GroupBy} from './group-by';
export {TimeSync} from './time-sync';
export {ExportCsv as ExhaustExportCsv} from './export-csv';
Loading