Skip to content

Commit b3d0b3d

Browse files
CLOUDP-294461: [Product Metrics/Observability] Implement Metric Collection workflow
1 parent 5cc211f commit b3d0b3d

File tree

3 files changed

+161
-0
lines changed

3 files changed

+161
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "mongodb-openapi",
33
"description": "MongoDB repository with OpenAPI specification",
4+
"type": "module",
45
"scripts": {
56
"format": "npx prettier . --write",
67
"format-check": "npx prettier . --check",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import spectral from '@stoplight/spectral-core';
2+
import { fileURLToPath } from 'node:url';
3+
import * as fs from 'node:fs';
4+
import * as path from 'node:path';
5+
import { loadOpenAPIFile, extractTeamOwnership, loadRuleset, loadCollectorResults, getSeverityPerRule, merge } from './utils.js';
6+
const { Spectral } = spectral;
7+
8+
//TBD
9+
const oasFile = '../../../../openapi/v2.json';
10+
const dirname = path.dirname(fileURLToPath(import.meta.url));
11+
const collectorResultsFile = path.join(dirname, '../ipa-collector-results-combined.log');
12+
13+
14+
async function runMetricCollectionJob() {
15+
try {
16+
console.log('Loading OpenAPI file...');
17+
const oasContent = loadOpenAPIFile(oasFile);
18+
19+
console.log('Extracting team ownership data...');
20+
const ownershipData = extractTeamOwnership(oasContent);
21+
22+
console.log('Initializing Spectral...');
23+
const spectral = new Spectral();
24+
const rulesetPath = path.join(dirname, '../ipa-spectral.yaml');
25+
const ruleset = await loadRuleset(rulesetPath, spectral);
26+
27+
console.log('Getting rule severities...');
28+
const ruleSeverityMap = getSeverityPerRule(ruleset);
29+
30+
console.log('Running Spectral analysis...');
31+
const spectralResults = await spectral.run(oasContent);
32+
33+
console.log('Loading collector results...');
34+
const collectorResults = loadCollectorResults(collectorResultsFile);
35+
36+
console.log('Merging results...');
37+
const mergedResults = merge(spectralResults, ownershipData, collectorResults, ruleSeverityMap);
38+
39+
console.log('Metric collection job complete.');
40+
return mergedResults;
41+
} catch (error) {
42+
console.error('Error during metric collection:', error.message);
43+
throw error;
44+
}
45+
}
46+
47+
48+
runMetricCollectionJob()
49+
.then((results) => fs.writeFileSync('results.log', JSON.stringify(results)))
50+
.catch((error) => console.error(error.message));
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import fs from 'node:fs';
2+
import {
3+
bundleAndLoadRuleset
4+
} from '@stoplight/spectral-ruleset-bundler/with-loader';
5+
import { EntryType } from './collector.js';
6+
7+
export function loadOpenAPIFile(filePath) {
8+
try {
9+
const content = fs.readFileSync(filePath, 'utf8');
10+
return JSON.parse(content);
11+
} catch (error) {
12+
throw new Error(`Failed to load OpenAPI file: ${error.message}`);
13+
}
14+
}
15+
16+
export function getSeverityPerRule(ruleset) {
17+
const rules = ruleset.rules || {};
18+
const map = {};
19+
for (const [name, ruleObject] of Object.entries(rules)) {
20+
map[name] = ruleObject.definition.severity;
21+
}
22+
return map;
23+
}
24+
25+
export async function loadRuleset(rulesetPath, spectral) {
26+
try {
27+
const ruleset = await bundleAndLoadRuleset(rulesetPath, { fs, fetch });
28+
await spectral.setRuleset(ruleset);
29+
return ruleset;
30+
} catch (error) {
31+
throw new Error(`Failed to load ruleset: ${error.message}`);
32+
}
33+
}
34+
35+
export function extractTeamOwnership(oasContent) {
36+
const ownerTeams = {};
37+
const paths = oasContent.paths || {};
38+
39+
for (const [path, pathItem] of Object.entries(paths)) {
40+
for (const [, operation] of Object.entries(pathItem)) {
41+
const ownerTeam = operation['x-xgen-owner-team'];
42+
43+
if (ownerTeam) {
44+
if (!ownerTeams[path]) {
45+
ownerTeams[path] = ownerTeam;
46+
} else if (ownerTeams[path] !== ownerTeam) {
47+
console.warn(`Conflict on path ${path}: ${ownerTeams[path]} vs ${ownerTeam}`);
48+
}
49+
}
50+
}
51+
}
52+
53+
return ownerTeams;
54+
}
55+
56+
export function loadCollectorResults(collectorResultsFilePath) {
57+
try {
58+
const content = fs.readFileSync(collectorResultsFilePath, 'utf8');
59+
const contentParsed = JSON.parse(content);
60+
return {
61+
[EntryType.VIOLATION]: contentParsed[EntryType.VIOLATION],
62+
[EntryType.ADOPTION]: contentParsed[EntryType.ADOPTION],
63+
[EntryType.EXCEPTION]: contentParsed[EntryType.EXCEPTION],
64+
};
65+
} catch (error) {
66+
throw new Error(`Failed to load Collector Results file: ${error.message}`);
67+
}
68+
}
69+
70+
function getIPAFromIPARule(ipaRule) {
71+
const pattern = /IPA-\d{3}/;
72+
const match = ipaRule.match(pattern);
73+
if (match) {
74+
return match[0];
75+
}
76+
}
77+
78+
export function merge(spectralResults, ownershipData, collectorResults, ruleSeverityMap) {
79+
const results = [];
80+
81+
function addEntry(entryType, adoptionStatus) {
82+
for (const entry of collectorResults[entryType]) {
83+
const existing = results.find((result) =>
84+
result.component_id === entry.componentId && result.ipa_rule === entry.ruleName
85+
);
86+
87+
if (existing) {
88+
console.warn('Duplicate entries found', existing);
89+
continue;
90+
}
91+
92+
results.push({
93+
component_id: entry.componentId,
94+
ipa_rule: entry.ruleName,
95+
ipa: getIPAFromIPARule(entry.ruleName),
96+
severity_level: ruleSeverityMap[entry.ruleName],
97+
adoption_status: adoptionStatus,
98+
exception_reason: entryType === EntryType.EXCEPTION ? entry.exceptionReason : null,
99+
owner_team: entry.ownerTeam || null,
100+
timestamp: new Date().toISOString(),
101+
});
102+
}
103+
}
104+
105+
addEntry(EntryType.VIOLATION, 'violated');
106+
addEntry(EntryType.ADOPTION, 'adopted');
107+
addEntry(EntryType.EXCEPTION, 'exempted');
108+
109+
return results;
110+
}

0 commit comments

Comments
 (0)